blob: b00569f254ebe3570a36cb7838ccc559f476a5f4 [file] [log] [blame]
divyadesai81bb7ba2020-03-11 11:45:23 +00001/*
2 * Copyright 2018-present Open Networking Foundation
3
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7
8 * http://www.apache.org/licenses/LICENSE-2.0
9
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
divyadesai52dc0882020-03-19 06:38:11 +000017// Package Config provides dynamic logging configuration for specific Voltha component with loglevel lookup
Rohan Agrawal00d3a412020-04-22 10:51:39 +000018// from etcd kvstore implemented using Backend.
divyadesai52dc0882020-03-19 06:38:11 +000019// Any Voltha component can start utilizing dynamic logging by starting goroutine of StartLogLevelConfigProcessing after
20// starting kvClient for the component.
divyadesai81bb7ba2020-03-11 11:45:23 +000021
22package config
23
24import (
25 "context"
26 "crypto/md5"
27 "encoding/json"
28 "errors"
29 "github.com/opencord/voltha-lib-go/v3/pkg/log"
30 "os"
Rohan Agrawal00d3a412020-04-22 10:51:39 +000031 "sort"
divyadesai81bb7ba2020-03-11 11:45:23 +000032 "strings"
33)
34
divyadesai52dc0882020-03-19 06:38:11 +000035const (
Rohan Agrawal00d3a412020-04-22 10:51:39 +000036 defaultLogLevelKey = "default" // kvstore key containing default loglevel
37 globalConfigRootNode = "global" // Root Node in kvstore containing global config
38 initialGlobalDefaultLogLevelValue = "WARN" // Hard-coded Global Default loglevel pushed at PoD startup
39 logPackagesListKey = "log_package_list" // kvstore key containing list of allowed log packages
divyadesai52dc0882020-03-19 06:38:11 +000040)
41
divyadesai81bb7ba2020-03-11 11:45:23 +000042// ComponentLogController represents a Configuration for Logging Config of specific Voltha component type
43// It stores ComponentConfig and GlobalConfig of loglevel config of specific Voltha component type
44// For example,ComponentLogController instance will be created for rw-core component
45type ComponentLogController struct {
46 ComponentName string
47 componentNameConfig *ComponentConfig
48 GlobalConfig *ComponentConfig
49 configManager *ConfigManager
50 logHash [16]byte
divyadesai52dc0882020-03-19 06:38:11 +000051 initialLogLevel string // Initial default log level set by helm chart
divyadesai81bb7ba2020-03-11 11:45:23 +000052}
53
54func NewComponentLogController(cm *ConfigManager) (*ComponentLogController, error) {
55
Rohan Agrawal00d3a412020-04-22 10:51:39 +000056 logger.Debug("creating-new-component-log-controller")
divyadesai81bb7ba2020-03-11 11:45:23 +000057 componentName := os.Getenv("COMPONENT_NAME")
58 if componentName == "" {
59 return nil, errors.New("Unable to retrieve PoD Component Name from Runtime env")
60 }
61
divyadesai52dc0882020-03-19 06:38:11 +000062 var defaultLogLevel string
63 var err error
64 // Retrieve and save default log level; used for fallback if all loglevel config is cleared in etcd
65 if defaultLogLevel, err = log.LogLevelToString(log.GetDefaultLogLevel()); err != nil {
66 defaultLogLevel = "DEBUG"
67 }
68
divyadesai81bb7ba2020-03-11 11:45:23 +000069 return &ComponentLogController{
70 ComponentName: componentName,
71 componentNameConfig: nil,
72 GlobalConfig: nil,
73 configManager: cm,
divyadesai52dc0882020-03-19 06:38:11 +000074 initialLogLevel: defaultLogLevel,
divyadesai81bb7ba2020-03-11 11:45:23 +000075 }, nil
76
77}
78
divyadesai52dc0882020-03-19 06:38:11 +000079// StartLogLevelConfigProcessing initialize component config and global config
80// Then, it persists initial default Loglevels into Config Store before
81// starting the loading and processing of all Log Configuration
82func StartLogLevelConfigProcessing(cm *ConfigManager, ctx context.Context) {
divyadesai81bb7ba2020-03-11 11:45:23 +000083 cc, err := NewComponentLogController(cm)
84 if err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +000085 logger.Errorw("unable-to-construct-component-log-controller-instance-for-log-config-monitoring", log.Fields{"error": err})
divyadesai81bb7ba2020-03-11 11:45:23 +000086 return
87 }
88
divyadesai52dc0882020-03-19 06:38:11 +000089 cc.GlobalConfig = cm.InitComponentConfig(globalConfigRootNode, ConfigTypeLogLevel)
Rohan Agrawal00d3a412020-04-22 10:51:39 +000090 logger.Debugw("global-log-config", log.Fields{"cc-global-config": cc.GlobalConfig})
divyadesai81bb7ba2020-03-11 11:45:23 +000091
92 cc.componentNameConfig = cm.InitComponentConfig(cc.ComponentName, ConfigTypeLogLevel)
Rohan Agrawal00d3a412020-04-22 10:51:39 +000093 logger.Debugw("component-log-config", log.Fields{"cc-component-name-config": cc.componentNameConfig})
divyadesai81bb7ba2020-03-11 11:45:23 +000094
divyadesai52dc0882020-03-19 06:38:11 +000095 cc.persistInitialDefaultLogConfigs(ctx)
96
Rohan Agrawal00d3a412020-04-22 10:51:39 +000097 cc.persistRegisteredLogPackageList(ctx)
98
divyadesai81bb7ba2020-03-11 11:45:23 +000099 cc.processLogConfig(ctx)
100}
101
divyadesai52dc0882020-03-19 06:38:11 +0000102// Method to persist Global default loglevel into etcd, if not set yet
103// It also checks and set Component default loglevel into etcd with initial loglevel set from command line
104func (c *ComponentLogController) persistInitialDefaultLogConfigs(ctx context.Context) {
105
106 _, err := c.GlobalConfig.Retrieve(ctx, defaultLogLevelKey)
107 if err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000108 logger.Debugw("failed-to-retrieve-global-default-log-config-at-startup", log.Fields{"error": err})
divyadesai52dc0882020-03-19 06:38:11 +0000109
110 err = c.GlobalConfig.Save(ctx, defaultLogLevelKey, initialGlobalDefaultLogLevelValue)
111 if err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000112 logger.Errorw("failed-to-persist-global-default-log-config-at-startup", log.Fields{"error": err, "loglevel": initialGlobalDefaultLogLevelValue})
divyadesai52dc0882020-03-19 06:38:11 +0000113 }
114 }
115
116 _, err = c.componentNameConfig.Retrieve(ctx, defaultLogLevelKey)
117 if err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000118 logger.Debugw("failed-to-retrieve-component-default-log-config-at-startup", log.Fields{"error": err})
divyadesai52dc0882020-03-19 06:38:11 +0000119
120 err = c.componentNameConfig.Save(ctx, defaultLogLevelKey, c.initialLogLevel)
121 if err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000122 logger.Errorw("failed-to-persist-component-default-log-config-at-startup", log.Fields{"error": err, "loglevel": c.initialLogLevel})
divyadesai52dc0882020-03-19 06:38:11 +0000123 }
124 }
125}
126
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000127// Method to save list of all registered packages for component into config kvstore. A single string
128// is constructed with comma-separated package names in sorted order and persisted
129func (c *ComponentLogController) persistRegisteredLogPackageList(ctx context.Context) {
130
131 componentMetadataConfig := c.configManager.InitComponentConfig(c.ComponentName, ConfigTypeMetadata)
132 logger.Debugw("component-metadata-config", log.Fields{"component-metadata-config": componentMetadataConfig})
133
134 packageList := log.GetPackageNames()
135 packageList = append(packageList, defaultLogLevelKey)
136 sort.Strings(packageList)
137
138 packageNames, err := json.Marshal(packageList)
139 if err != nil {
140 logger.Errorw("failed-to-marshal-log-package-list-for-storage", log.Fields{"error": err, "packageList": packageList})
141 return
142 }
143
144 if err := componentMetadataConfig.Save(ctx, logPackagesListKey, string(packageNames)); err != nil {
145 logger.Errorw("failed-to-persist-component-registered-log-package-list-at-startup", log.Fields{"error": err, "packageNames": packageNames})
146 }
147}
148
divyadesai52dc0882020-03-19 06:38:11 +0000149// ProcessLogConfig will first load and apply log config and then start waiting on component config and global config
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000150// channels for any changes. Event channel will be recieved from Backend for valid change type
151// Then data for componentn log config and global log config will be retrieved from Backend and stored in updatedLogConfig in precedence order
divyadesai81bb7ba2020-03-11 11:45:23 +0000152// If any changes in updatedLogConfig will be applied on component
153func (c *ComponentLogController) processLogConfig(ctx context.Context) {
154
divyadesai52dc0882020-03-19 06:38:11 +0000155 // Load and apply Log Config for first time
156 initialLogConfig, err := c.buildUpdatedLogConfig(ctx)
157 if err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000158 logger.Warnw("unable-to-load-log-config-at-startup", log.Fields{"error": err})
divyadesai52dc0882020-03-19 06:38:11 +0000159 } else {
160 if err := c.loadAndApplyLogConfig(initialLogConfig); err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000161 logger.Warnw("unable-to-apply-log-config-at-startup", log.Fields{"error": err})
divyadesai52dc0882020-03-19 06:38:11 +0000162 }
163 }
164
divyadesai81bb7ba2020-03-11 11:45:23 +0000165 componentConfigEventChan := c.componentNameConfig.MonitorForConfigChange(ctx)
166
167 globalConfigEventChan := c.GlobalConfig.MonitorForConfigChange(ctx)
168
169 // process the events for componentName and global config
170 var configEvent *ConfigChangeEvent
171 for {
172 select {
173 case configEvent = <-globalConfigEventChan:
174 case configEvent = <-componentConfigEventChan:
175
176 }
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000177 logger.Debugw("processing-log-config-change", log.Fields{"ChangeType": configEvent.ChangeType, "Package": configEvent.ConfigAttribute})
divyadesai81bb7ba2020-03-11 11:45:23 +0000178
179 updatedLogConfig, err := c.buildUpdatedLogConfig(ctx)
180 if err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000181 logger.Warnw("unable-to-fetch-updated-log-config", log.Fields{"error": err})
divyadesai81bb7ba2020-03-11 11:45:23 +0000182 continue
183 }
184
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000185 logger.Debugw("applying-updated-log-config", log.Fields{"updated-log-config": updatedLogConfig})
divyadesai81bb7ba2020-03-11 11:45:23 +0000186
187 if err := c.loadAndApplyLogConfig(updatedLogConfig); err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000188 logger.Warnw("unable-to-load-and-apply-log-config", log.Fields{"error": err})
divyadesai81bb7ba2020-03-11 11:45:23 +0000189 }
190 }
191
192}
193
194// get active loglevel from the zap logger
divyadesai52dc0882020-03-19 06:38:11 +0000195func getActiveLogLevels() map[string]string {
196 loglevels := make(map[string]string)
divyadesai81bb7ba2020-03-11 11:45:23 +0000197
198 // now do the default log level
199 if level, err := log.LogLevelToString(log.GetDefaultLogLevel()); err == nil {
divyadesai52dc0882020-03-19 06:38:11 +0000200 loglevels[defaultLogLevelKey] = level
divyadesai81bb7ba2020-03-11 11:45:23 +0000201 }
202
203 // do the per-package log levels
204 for _, packageName := range log.GetPackageNames() {
205 level, err := log.GetPackageLogLevel(packageName)
206 if err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000207 logger.Warnw("unable-to-fetch-current-active-loglevel-for-package-name", log.Fields{"package-name": packageName, "error": err})
divyadesai52dc0882020-03-19 06:38:11 +0000208 continue
divyadesai81bb7ba2020-03-11 11:45:23 +0000209 }
210
divyadesai81bb7ba2020-03-11 11:45:23 +0000211 if l, err := log.LogLevelToString(level); err == nil {
divyadesai52dc0882020-03-19 06:38:11 +0000212 loglevels[packageName] = l
divyadesai81bb7ba2020-03-11 11:45:23 +0000213 }
divyadesai81bb7ba2020-03-11 11:45:23 +0000214 }
divyadesai81bb7ba2020-03-11 11:45:23 +0000215
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000216 logger.Debugw("retreived-log-levels-from-zap-logger", log.Fields{"loglevels": loglevels})
divyadesai52dc0882020-03-19 06:38:11 +0000217
218 return loglevels
divyadesai81bb7ba2020-03-11 11:45:23 +0000219}
220
221func (c *ComponentLogController) getGlobalLogConfig(ctx context.Context) (string, error) {
222
divyadesai52dc0882020-03-19 06:38:11 +0000223 globalDefaultLogLevel, err := c.GlobalConfig.Retrieve(ctx, defaultLogLevelKey)
divyadesai81bb7ba2020-03-11 11:45:23 +0000224 if err != nil {
225 return "", err
226 }
227
divyadesai52dc0882020-03-19 06:38:11 +0000228 // Handle edge cases when global default loglevel is deleted directly from etcd or set to a invalid value
229 // We should use hard-coded initial default value in such cases
230 if globalDefaultLogLevel == "" {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000231 logger.Warn("global-default-loglevel-not-found-in-config-store")
divyadesai52dc0882020-03-19 06:38:11 +0000232 globalDefaultLogLevel = initialGlobalDefaultLogLevelValue
divyadesai81bb7ba2020-03-11 11:45:23 +0000233 }
divyadesai52dc0882020-03-19 06:38:11 +0000234
235 if _, err := log.StringToLogLevel(globalDefaultLogLevel); err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000236 logger.Warnw("unsupported-loglevel-config-defined-at-global-default", log.Fields{"log-level": globalDefaultLogLevel})
divyadesai52dc0882020-03-19 06:38:11 +0000237 globalDefaultLogLevel = initialGlobalDefaultLogLevelValue
238 }
239
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000240 logger.Debugw("retrieved-global-default-loglevel", log.Fields{"level": globalDefaultLogLevel})
divyadesai81bb7ba2020-03-11 11:45:23 +0000241
242 return globalDefaultLogLevel, nil
243}
244
divyadesai52dc0882020-03-19 06:38:11 +0000245func (c *ComponentLogController) getComponentLogConfig(ctx context.Context, globalDefaultLogLevel string) (map[string]string, error) {
divyadesai81bb7ba2020-03-11 11:45:23 +0000246 componentLogConfig, err := c.componentNameConfig.RetrieveAll(ctx)
247 if err != nil {
248 return nil, err
249 }
250
divyadesai52dc0882020-03-19 06:38:11 +0000251 effectiveDefaultLogLevel := ""
252 for logConfigKey, logConfigValue := range componentLogConfig {
253 if _, err := log.StringToLogLevel(logConfigValue); err != nil || logConfigKey == "" {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000254 logger.Warnw("unsupported-loglevel-config-defined-at-component-context", log.Fields{"package-name": logConfigKey, "log-level": logConfigValue})
divyadesai52dc0882020-03-19 06:38:11 +0000255 delete(componentLogConfig, logConfigKey)
divyadesai81bb7ba2020-03-11 11:45:23 +0000256 } else {
divyadesai52dc0882020-03-19 06:38:11 +0000257 if logConfigKey == defaultLogLevelKey {
258 effectiveDefaultLogLevel = componentLogConfig[defaultLogLevelKey]
divyadesai81bb7ba2020-03-11 11:45:23 +0000259 }
260 }
261 }
divyadesai52dc0882020-03-19 06:38:11 +0000262
263 // if default loglevel is not configured for the component, component should use
264 // default loglevel configured at global level
265 if effectiveDefaultLogLevel == "" {
266 effectiveDefaultLogLevel = globalDefaultLogLevel
divyadesai81bb7ba2020-03-11 11:45:23 +0000267 }
divyadesai52dc0882020-03-19 06:38:11 +0000268
269 componentLogConfig[defaultLogLevelKey] = effectiveDefaultLogLevel
270
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000271 logger.Debugw("retrieved-component-log-config", log.Fields{"component-log-level": componentLogConfig})
divyadesai81bb7ba2020-03-11 11:45:23 +0000272
273 return componentLogConfig, nil
274}
275
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000276// buildUpdatedLogConfig retrieve the global logConfig and component logConfig from Backend
divyadesai81bb7ba2020-03-11 11:45:23 +0000277// component logConfig stores the log config with precedence order
278// For example, If the global logConfig is set and component logConfig is set only for specific package then
279// component logConfig is stored with global logConfig and component logConfig of specific package
280// For example, If the global logConfig is set and component logConfig is set for specific package and as well as for default then
281// component logConfig is stored with component logConfig data only
282func (c *ComponentLogController) buildUpdatedLogConfig(ctx context.Context) (map[string]string, error) {
283 globalLogLevel, err := c.getGlobalLogConfig(ctx)
284 if err != nil {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000285 logger.Errorw("unable-to-retrieve-global-log-config", log.Fields{"err": err})
divyadesai81bb7ba2020-03-11 11:45:23 +0000286 }
287
divyadesai52dc0882020-03-19 06:38:11 +0000288 componentLogConfig, err := c.getComponentLogConfig(ctx, globalLogLevel)
divyadesai81bb7ba2020-03-11 11:45:23 +0000289 if err != nil {
290 return nil, err
291 }
292
divyadesai52dc0882020-03-19 06:38:11 +0000293 finalLogConfig := make(map[string]string)
294 for packageName, logLevel := range componentLogConfig {
295 finalLogConfig[strings.ReplaceAll(packageName, "#", "/")] = logLevel
296 }
297
298 return finalLogConfig, nil
divyadesai81bb7ba2020-03-11 11:45:23 +0000299}
300
301// load and apply the current configuration for component name
302// create hash of loaded configuration using GenerateLogConfigHash
303// if there is previous hash stored, compare the hash to stored hash
304// if there is any change will call UpdateLogLevels
305func (c *ComponentLogController) loadAndApplyLogConfig(logConfig map[string]string) error {
306 currentLogHash, err := GenerateLogConfigHash(logConfig)
307 if err != nil {
308 return err
309 }
310
divyadesai81bb7ba2020-03-11 11:45:23 +0000311 if c.logHash != currentLogHash {
312 UpdateLogLevels(logConfig)
313 c.logHash = currentLogHash
divyadesai52dc0882020-03-19 06:38:11 +0000314 } else {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000315 logger.Debug("effective-loglevel-config-same-as-currently-active")
divyadesai81bb7ba2020-03-11 11:45:23 +0000316 }
divyadesai52dc0882020-03-19 06:38:11 +0000317
divyadesai81bb7ba2020-03-11 11:45:23 +0000318 return nil
319}
320
divyadesai52dc0882020-03-19 06:38:11 +0000321// createModifiedLogLevels loops through the activeLogLevels recieved from zap logger and updatedLogLevels recieved from buildUpdatedLogConfig
322// to identify and create map of modified Log Levels of 2 types:
323// - Packages for which log level has been changed
324// - Packages for which log level config has been cleared - set to default log level
325func createModifiedLogLevels(activeLogLevels, updatedLogLevels map[string]string) map[string]string {
326 defaultLevel := updatedLogLevels[defaultLogLevelKey]
divyadesai81bb7ba2020-03-11 11:45:23 +0000327
divyadesai52dc0882020-03-19 06:38:11 +0000328 modifiedLogLevels := make(map[string]string)
divyadesai81bb7ba2020-03-11 11:45:23 +0000329 for activeKey, activeLevel := range activeLogLevels {
330 if _, exist := updatedLogLevels[activeKey]; !exist {
divyadesai52dc0882020-03-19 06:38:11 +0000331 if activeLevel != defaultLevel {
332 modifiedLogLevels[activeKey] = defaultLevel
divyadesai81bb7ba2020-03-11 11:45:23 +0000333 }
divyadesai52dc0882020-03-19 06:38:11 +0000334 } else if activeLevel != updatedLogLevels[activeKey] {
335 modifiedLogLevels[activeKey] = updatedLogLevels[activeKey]
divyadesai81bb7ba2020-03-11 11:45:23 +0000336 }
337 }
divyadesai52dc0882020-03-19 06:38:11 +0000338
339 // Log warnings for all invalid packages for which log config has been set
340 for key, value := range updatedLogLevels {
341 if _, exist := activeLogLevels[key]; !exist {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000342 logger.Warnw("ignoring-loglevel-set-for-invalid-package", log.Fields{"package": key, "log-level": value})
divyadesai52dc0882020-03-19 06:38:11 +0000343 }
344 }
345
346 return modifiedLogLevels
divyadesai81bb7ba2020-03-11 11:45:23 +0000347}
348
349// updateLogLevels update the loglevels for the component
350// retrieve active confguration from logger
351// compare with entries one by one and apply
divyadesai52dc0882020-03-19 06:38:11 +0000352func UpdateLogLevels(updatedLogConfig map[string]string) {
divyadesai81bb7ba2020-03-11 11:45:23 +0000353
divyadesai52dc0882020-03-19 06:38:11 +0000354 activeLogLevels := getActiveLogLevels()
355 changedLogLevels := createModifiedLogLevels(activeLogLevels, updatedLogConfig)
356
357 // If no changed log levels are found, just return. It may happen on configuration of a invalid package
358 if len(changedLogLevels) == 0 {
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000359 logger.Debug("no-change-in-effective-loglevel-config")
divyadesai52dc0882020-03-19 06:38:11 +0000360 return
361 }
362
Rohan Agrawal00d3a412020-04-22 10:51:39 +0000363 logger.Debugw("applying-log-level-for-modified-packages", log.Fields{"changed-log-levels": changedLogLevels})
divyadesai52dc0882020-03-19 06:38:11 +0000364 for key, level := range changedLogLevels {
365 if key == defaultLogLevelKey {
divyadesai81bb7ba2020-03-11 11:45:23 +0000366 if l, err := log.StringToLogLevel(level); err == nil {
367 log.SetDefaultLogLevel(l)
368 }
369 } else {
divyadesai81bb7ba2020-03-11 11:45:23 +0000370 if l, err := log.StringToLogLevel(level); err == nil {
divyadesai52dc0882020-03-19 06:38:11 +0000371 log.SetPackageLogLevel(key, l)
divyadesai81bb7ba2020-03-11 11:45:23 +0000372 }
373 }
374 }
divyadesai81bb7ba2020-03-11 11:45:23 +0000375}
376
377// generate md5 hash of key value pairs appended into a single string
378// in order by key name
379func GenerateLogConfigHash(createHashLog map[string]string) ([16]byte, error) {
380 createHashLogBytes := []byte{}
381 levelData, err := json.Marshal(createHashLog)
382 if err != nil {
383 return [16]byte{}, err
384 }
385 createHashLogBytes = append(createHashLogBytes, levelData...)
386 return md5.Sum(createHashLogBytes), nil
387}