internal/logger/logger.go (132 lines of code) (raw):

// Copyright 2024 Google LLC // // 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 logger wraps the galog configuration/initialization. package logger import ( "context" "fmt" "os" "path/filepath" "regexp" "time" "github.com/GoogleCloudPlatform/agentcommunication_client" "github.com/GoogleCloudPlatform/galog" "github.com/GoogleCloudPlatform/google-guest-agent/internal/events" "github.com/GoogleCloudPlatform/google-guest-agent/internal/metadata" "github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file" ) // Options contains the loggers configuration/options. type Options struct { // CloudIdent is the cloud logging logId attribute - or logName field. CloudIdent string // Ident is the application ident used across loggers. Ident string // ProgramVersion is the program version. ProgramVersion string // LogFile is the path of the log file. LogFile string // LogToStderr flags if stderr loggers must be enabled. LogToStderr bool // LogToCloudLogging flags if cloud logging loggers must be enabled. LogToCloudLogging bool // cloudLoggingBackend is the cloud logging backend. cloudLoggingBackend *galog.CloudBackend // cloudLoggingWithoutAuthentication flags if cloud logging should be // initialized without authentication. Useful for testing. cloudLoggingWithoutAuthentication bool // Level is the log level. Level int // Verbosity is the log verbosity level. Verbosity int // Prefix is a prefix tag appended to all log entries, it's passed down to // galog configuration. Prefix string // ACSClientDebugLogging is a flag to enable ACS client logging. ACSClientDebugLogging bool } const ( // loggerMetadataSubscriberID is the subscriber ID for the logger metadata // event used to register the cloud logging backend - it can only be // registered after we got the first metadata event since cloud logging // requires at least the project name to be registered. loggerMetadataSubscriberID = "logger_metadata_subscriber" // cloudLoggingFlushCadence is the cadence of flushing cloud logging data. cloudLoggingFlushCadence = time.Second * 5 // CloudLoggingLogID is the logId used for cloud logging for core plugin. CloudLoggingLogID = "GCEGuestAgent" // LocalLoggerIdent is the ident used for local loggers (i.e syslog), it is // shared between core plugin and guest agent - they both use the same // "name space". LocalLoggerIdent = "google_guest_agent" // CorePluginLogPrefix is a human readable prefix added to all log entries // identifying the core plugin. CorePluginLogPrefix = "CorePlugin" // ManagerCloudLoggingLogID is the logId used for cloud logging for plugin // manager. ManagerCloudLoggingLogID = "GCEGuestAgentManager" // ManagerLocalLoggerIdent is the ident used for local loggers (i.e syslog) // for plugin manager. ManagerLocalLoggerIdent = "google_guest_agent_manager" // ManagerLogPrefix is a human readable prefix added to all log entries for // plugin manager. ManagerLogPrefix = "GCEGuestAgentManager" // The following are MIG labels added to the cloud logging logs. // MIGNameLabel is the MIG name label. migNameLabel = `compute.googleapis.com/instance_group_manager/name` // migZoneLabel is the MIG zone label. migZoneLabel = `compute.googleapis.com/instance_group_manager/zone` // migRegionLabel is the MIG region label. migRegionLabel = `compute.googleapis.com/instance_group_manager/region` ) // Init initializes the logger. func Init(ctx context.Context, opts Options) error { enabledLoggers, err := initPlatformLogger(ctx, opts.Ident, opts.Prefix) if err != nil { return fmt.Errorf("failed to initialize platform logger: %w", err) } galog.SetMinVerbosity(opts.Verbosity) if opts.LogFile != "" && file.Exists(filepath.Dir(opts.LogFile), file.TypeDir) { enabledLoggers = append(enabledLoggers, galog.NewFileBackend(opts.LogFile)) } if opts.LogToStderr { enabledLoggers = append(enabledLoggers, galog.NewStderrBackend(os.Stderr)) } for _, logger := range enabledLoggers { galog.RegisterBackend(ctx, logger) } if opts.LogToCloudLogging { // We initialize and register the cloud logging backend in a lazy mode, // meaning the cloud logging client will only be initialized when the // metadata longpoll event is handled by initCloudLogging() subscriber. be, err := galog.NewCloudBackend(ctx, galog.CloudLoggingInitModeLazy, nil) galog.RegisterBackend(ctx, be) if err != nil { return fmt.Errorf("failed to initialize cloud logging: %w", err) } opts.cloudLoggingBackend = be sub := events.EventSubscriber{Name: loggerMetadataSubscriberID, Data: &opts, Callback: initCloudLogging} events.FetchManager().Subscribe(metadata.LongpollEvent, sub) } level, err := galog.ParseLevel(opts.Level) if err != nil { return fmt.Errorf("invalid log level: %w", err) } galog.SetLevel(level) client.DebugLogging = opts.ACSClientDebugLogging return nil } // parseMIGLabels parses the MIG labels from the created-by metadata attribute. // This is used to add extra labels to the Cloud Logging logs. func parseMIGLabels(mds *metadata.Descriptor) (map[string]string, error) { labels := make(map[string]string) createdBy := mds.Instance().Attributes().CreatedBy() if createdBy == "" { return labels, nil } // Make sure the `created-by` is set by MIG. migRe, err := regexp.Compile(`^projects/[^/]+/(zones|regions)/([^/]+)/instanceGroupManagers/([^/]+)$`) if err != nil { return labels, err } migMatch := migRe.FindStringSubmatch(mds.Instance().Attributes().CreatedBy()) if migMatch == nil { return labels, nil } var locationLabel string switch migMatch[1] { case "zones": locationLabel = migZoneLabel case "regions": locationLabel = migRegionLabel } labels[migNameLabel] = migMatch[3] labels[locationLabel] = migMatch[2] return labels, nil } // initCloudLogging is a subscribed event handler to metadata event. Cloud // Logging initialization depends on data provided by metadata server, we can // only initialize if after having the first descriptor being available. This // handler/subscriber will never be renewed. func initCloudLogging(ctx context.Context, eventType string, data any, event *events.EventData) bool { // Any invalid data or event data will be dealt as self correctable error // meaning we return true to indicate the event should be retried. opts, ok := data.(*Options) if !ok { galog.Errorf("Failed to initialize cloud logging, invalid \"data\" type passed to event callback.") return true } mds, ok := event.Data.(*metadata.Descriptor) if !ok { galog.Errorf("Failed to initialize cloud logging, invalid \"event.Data\" type passed to event callback.") return true } extraLabels, err := parseMIGLabels(mds) if err != nil { galog.Errorf("Failed to parse MIG labels: %v", err) } programName := filepath.Base(os.Args[0]) cloudOpts := &galog.CloudOptions{ Ident: opts.CloudIdent, // Core plugin and guest agent use the same logId (here Ident), in a way // to differentiate log entries we add the ProgName to the log entry's // payload. ProgramName: programName, ProgramVersion: opts.ProgramVersion, Project: mds.Project().ID(), FlushCadence: cloudLoggingFlushCadence, Instance: mds.Instance().Name(), WithoutAuthentication: opts.cloudLoggingWithoutAuthentication, ExtraLabels: extraLabels, } if err := opts.cloudLoggingBackend.InitClient(ctx, cloudOpts); err != nil { galog.Errorf("failed to initialize cloud logging (%s): %v", err, programName) return true } galog.Infof("Cloud logging initialized (%s).", programName) return false }