blob: b45c2c814ef288712710c1afa0711e313c953fb7 [file] [log] [blame]
/*
* Copyright 2018-present Open Networking Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package Config provides dynamic logging configuration for specific Voltha component type implemented using backend.The package can be used in following manner
// Any Voltha component type can start dynamic logging by starting goroutine of ProcessLogConfigChange after starting kvClient for the component.
package config
import (
"context"
"crypto/md5"
"encoding/json"
"errors"
"github.com/opencord/voltha-lib-go/v3/pkg/log"
"os"
"strings"
)
// ComponentLogController represents a Configuration for Logging Config of specific Voltha component type
// It stores ComponentConfig and GlobalConfig of loglevel config of specific Voltha component type
// For example,ComponentLogController instance will be created for rw-core component
type ComponentLogController struct {
ComponentName string
componentNameConfig *ComponentConfig
GlobalConfig *ComponentConfig
configManager *ConfigManager
logHash [16]byte
}
func NewComponentLogController(cm *ConfigManager) (*ComponentLogController, error) {
log.Debug("creating-new-component-log-controller")
componentName := os.Getenv("COMPONENT_NAME")
if componentName == "" {
return nil, errors.New("Unable to retrieve PoD Component Name from Runtime env")
}
return &ComponentLogController{
ComponentName: componentName,
componentNameConfig: nil,
GlobalConfig: nil,
configManager: cm,
}, nil
}
// ProcessLogConfigChange initialize component config and global config
func ProcessLogConfigChange(cm *ConfigManager, ctx context.Context) {
cc, err := NewComponentLogController(cm)
if err != nil {
log.Errorw("unable-to-construct-component-log-controller-instance-for-log-config-monitoring", log.Fields{"error": err})
return
}
log.Debugw("processing-log-config-change", log.Fields{"cc": cc})
cc.GlobalConfig = cm.InitComponentConfig("global", ConfigTypeLogLevel)
log.Debugw("global-log-config", log.Fields{"cc-global-config": cc.GlobalConfig})
cc.componentNameConfig = cm.InitComponentConfig(cc.ComponentName, ConfigTypeLogLevel)
log.Debugw("component-log-config", log.Fields{"cc-component-name-config": cc.componentNameConfig})
cc.processLogConfig(ctx)
}
// ProcessLogConfig wait on componentn config and global config channel for any changes
// Event channel will be recieved from backend for valid change type
// Then data for componentn log config and global log config will be retrieved from backend and stored in updatedLogConfig in precedence order
// If any changes in updatedLogConfig will be applied on component
func (c *ComponentLogController) processLogConfig(ctx context.Context) {
componentConfigEventChan := c.componentNameConfig.MonitorForConfigChange(ctx)
globalConfigEventChan := c.GlobalConfig.MonitorForConfigChange(ctx)
// process the events for componentName and global config
var configEvent *ConfigChangeEvent
for {
select {
case configEvent = <-globalConfigEventChan:
case configEvent = <-componentConfigEventChan:
}
log.Debugw("processing-log-config-change", log.Fields{"config-event": configEvent})
updatedLogConfig, err := c.buildUpdatedLogConfig(ctx)
if err != nil {
log.Warnw("unable-to-fetch-updated-log-config", log.Fields{"error": err})
continue
}
log.Debugw("applying-updated-log-config", log.Fields{"updated-log-config": updatedLogConfig})
if err := c.loadAndApplyLogConfig(updatedLogConfig); err != nil {
log.Warnw("unable-to-load-and-apply-log-config", log.Fields{"error": err})
}
}
}
// get active loglevel from the zap logger
func getActiveLogLevel() map[string]string {
loglevel := make(map[string]string)
// now do the default log level
if level, err := log.LogLevelToString(log.GetDefaultLogLevel()); err == nil {
loglevel["default"] = level
}
// do the per-package log levels
for _, packageName := range log.GetPackageNames() {
level, err := log.GetPackageLogLevel(packageName)
if err != nil {
log.Warnw("unable-to-fetch-current-active-loglevel-for-package-name", log.Fields{"package-name": packageName, "error": err})
}
packagename := strings.ReplaceAll(packageName, "/", "#")
if l, err := log.LogLevelToString(level); err == nil {
loglevel[packagename] = l
}
}
log.Debugw("getting-log-levels-from-zap-logger", log.Fields{"log-level": loglevel})
return loglevel
}
func (c *ComponentLogController) getGlobalLogConfig(ctx context.Context) (string, error) {
globalDefaultLogLevel := ""
globalLogConfig, err := c.GlobalConfig.RetrieveAll(ctx)
if err != nil {
return "", err
}
if globalLevel, ok := globalLogConfig["default"]; ok {
if _, err := log.StringToLogLevel(globalLevel); err != nil {
log.Warnw("unsupported-loglevel-config-defined-at-global-context-pacakge-name", log.Fields{"log-level": globalLevel})
} else {
globalDefaultLogLevel = globalLevel
}
}
log.Debugw("retrieved-global-log-config", log.Fields{"global-log-config": globalLogConfig})
return globalDefaultLogLevel, nil
}
func (c *ComponentLogController) getComponentLogConfig(globalDefaultLogLevel string, ctx context.Context) (map[string]string, error) {
var defaultPresent bool
componentLogConfig, err := c.componentNameConfig.RetrieveAll(ctx)
if err != nil {
return nil, err
}
for componentKey, componentLevel := range componentLogConfig {
if _, err := log.StringToLogLevel(componentLevel); err != nil || componentKey == "" {
log.Warnw("unsupported-loglevel-config-defined-at-component-context", log.Fields{"package-name": componentKey, "log-level": componentLevel})
delete(componentLogConfig, componentKey)
} else {
if componentKey == "default" {
defaultPresent = true
}
}
}
if !defaultPresent {
if globalDefaultLogLevel != "" {
componentLogConfig["default"] = globalDefaultLogLevel
}
}
log.Debugw("retrieved-component-log-config", log.Fields{"component-log-level": componentLogConfig})
return componentLogConfig, nil
}
// buildUpdatedLogConfig retrieve the global logConfig and component logConfig from backend
// component logConfig stores the log config with precedence order
// For example, If the global logConfig is set and component logConfig is set only for specific package then
// component logConfig is stored with global logConfig and component logConfig of specific package
// For example, If the global logConfig is set and component logConfig is set for specific package and as well as for default then
// component logConfig is stored with component logConfig data only
func (c *ComponentLogController) buildUpdatedLogConfig(ctx context.Context) (map[string]string, error) {
globalLogLevel, err := c.getGlobalLogConfig(ctx)
if err != nil {
return nil, err
}
componentLogConfig, err := c.getComponentLogConfig(globalLogLevel, ctx)
if err != nil {
return nil, err
}
log.Debugw("building-and-updating-log-config", log.Fields{"component-log-config": componentLogConfig})
return componentLogConfig, nil
}
// load and apply the current configuration for component name
// create hash of loaded configuration using GenerateLogConfigHash
// if there is previous hash stored, compare the hash to stored hash
// if there is any change will call UpdateLogLevels
func (c *ComponentLogController) loadAndApplyLogConfig(logConfig map[string]string) error {
currentLogHash, err := GenerateLogConfigHash(logConfig)
if err != nil {
return err
}
log.Debugw("loading-and-applying-log-config", log.Fields{"log-config": logConfig})
if c.logHash != currentLogHash {
UpdateLogLevels(logConfig)
c.logHash = currentLogHash
}
return nil
}
// getDefaultLogLevel to return active default log level
func getDefaultLogLevel(logConfig map[string]string) string {
for key, level := range logConfig {
if key == "default" {
return level
}
}
return ""
}
// createCurrentLogLevel loop through the activeLogLevels recieved from zap logger and updatedLogLevels recieved from buildUpdatedLogConfig
// The packageName is present or not will be checked in updatedLogLevels ,if the package name is not present then updatedLogLevels will be updated with
// the packageName and loglevel with default log level
func createCurrentLogLevel(activeLogLevels, updatedLogLevels map[string]string) map[string]string {
level := getDefaultLogLevel(updatedLogLevels)
for activeKey, activeLevel := range activeLogLevels {
if _, exist := updatedLogLevels[activeKey]; !exist {
if level != "" {
activeLevel = level
}
updatedLogLevels[activeKey] = activeLevel
}
}
return updatedLogLevels
}
// updateLogLevels update the loglevels for the component
// retrieve active confguration from logger
// compare with entries one by one and apply
func UpdateLogLevels(logLevel map[string]string) {
activeLogLevels := getActiveLogLevel()
currentLogLevel := createCurrentLogLevel(activeLogLevels, logLevel)
for key, level := range currentLogLevel {
if key == "default" {
if l, err := log.StringToLogLevel(level); err == nil {
log.SetDefaultLogLevel(l)
}
} else {
pname := strings.ReplaceAll(key, "#", "/")
if _, err := log.AddPackage(log.JSON, log.DebugLevel, nil, pname); err != nil {
log.Warnw("unable-to-add-log-package", log.Fields{"package-name": pname, "error": err})
}
if l, err := log.StringToLogLevel(level); err == nil {
log.SetPackageLogLevel(pname, l)
}
}
}
log.Debugw("updated-log-level", log.Fields{"current-log-level": currentLogLevel})
}
// generate md5 hash of key value pairs appended into a single string
// in order by key name
func GenerateLogConfigHash(createHashLog map[string]string) ([16]byte, error) {
createHashLogBytes := []byte{}
levelData, err := json.Marshal(createHashLog)
if err != nil {
return [16]byte{}, err
}
createHashLogBytes = append(createHashLogBytes, levelData...)
return md5.Sum(createHashLogBytes), nil
}