internal/hostmetrics/hostmetrics.go (153 lines of code) (raw):

/* Copyright 2022 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 https://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 hostmetrics provides Google Cloud VM metric information for the SAP host agent to collect. package hostmetrics import ( "context" "fmt" "net/http" "os" "runtime" "sync" "time" "github.com/GoogleCloudPlatform/sapagent/internal/heartbeat" "github.com/GoogleCloudPlatform/sapagent/internal/hostmetrics/agenttime" "github.com/GoogleCloudPlatform/sapagent/internal/hostmetrics/cloudmetricreader" "github.com/GoogleCloudPlatform/sapagent/internal/hostmetrics/configurationmetricreader" "github.com/GoogleCloudPlatform/sapagent/internal/hostmetrics/cpustatsreader" "github.com/GoogleCloudPlatform/sapagent/internal/hostmetrics/diskstatsreader" "github.com/GoogleCloudPlatform/sapagent/internal/hostmetrics/memorymetricreader" "github.com/GoogleCloudPlatform/sapagent/internal/hostmetrics/osmetricreader" "github.com/GoogleCloudPlatform/sapagent/internal/instanceinfo" "github.com/GoogleCloudPlatform/sapagent/internal/usagemetrics" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/commandlineexecutor" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/recovery" cpb "github.com/GoogleCloudPlatform/sapagent/protos/configuration" mpb "github.com/GoogleCloudPlatform/sapagent/protos/metrics" ) // Parameters holds the parameters for all of the Collect* function calls. type Parameters struct { Config *cpb.Configuration InstanceInfoReader instanceinfo.Reader CloudMetricReader cloudmetricreader.CloudMetricReader AgentTime agenttime.AgentTime HeartbeatSpec *heartbeat.Spec } type hostMetricsReaders struct { configmr *configurationmetricreader.ConfigMetricReader cpusr *cpustatsreader.Reader mmr *memorymetricreader.Reader dsr *diskstatsreader.Reader } var ( metricsXML string = "<metrics></metrics>" httpServerRoutine *recovery.RecoverableRoutine collectHostMetricsRoutine *recovery.RecoverableRoutine dailyUsageRoutineStarted sync.Once ) func requestHandler(w http.ResponseWriter, r *http.Request) { log.Logger.Debug("Writing metrics XML response") fmt.Fprint(w, metricsXML) } // StartSAPHostAgentProvider will startup the http server and collect metrics for the sap host agent // if enabled in the configuration. Returns true if the collection goroutine is started, and false otherwise. func StartSAPHostAgentProvider(ctx context.Context, cancel context.CancelFunc, restarting bool, params Parameters) bool { if !params.Config.GetProvideSapHostAgentMetrics().GetValue() { log.CtxLogger(ctx).Info("Not providing SAP Host Agent metrics") return false } // This routine runs forever (without respecting ctx cancellation) and does not need to be restarted. httpServerRoutine = &recovery.RecoverableRoutine{ Routine: runHTTPServer, RoutineArg: cancel, ErrorCode: usagemetrics.HostMetricsHTTPServerRoutineFailure, UsageLogger: *usagemetrics.Logger, ExpectedMinDuration: time.Minute, } if restarting { log.CtxLogger(ctx).Debug("Not starting HTTP server routine as it is already running") } else { log.CtxLogger(ctx).Debug("Starting HTTP server routine") httpServerRoutine.StartRoutine(ctx) } collectHostMetricsRoutine = &recovery.RecoverableRoutine{ Routine: collectHostMetrics, RoutineArg: params, ErrorCode: usagemetrics.HostMetricsCollectionRoutineFailure, UsageLogger: *usagemetrics.Logger, ExpectedMinDuration: time.Minute, } collectHostMetricsRoutine.StartRoutine(ctx) return true } // runHTTPServer starts an HTTP server on localhost:18181 that stays alive forever. func runHTTPServer(ctx context.Context, a any) { var cancel context.CancelFunc if v, ok := a.(context.CancelFunc); ok { cancel = v } else { log.CtxLogger(ctx).Info("Host Metrics Context arg is not a context.CancelFunc") return } http.HandleFunc("/", requestHandler) if err := http.ListenAndServe("localhost:18181", nil); err != nil { usagemetrics.Error(usagemetrics.LocalHTTPListenerCreateFailure) // Could not create HTTP listener log.CtxLogger(ctx).Errorw("Could not start HTTP server on localhost:18181", "error", log.Error(err)) log.CtxLogger(ctx).Info("Cancelling Host Metrics Context") cancel() return } log.CtxLogger(ctx).Info("HTTP server listening on localhost:18181 for SAP Host Agent connections") } // collectHostMetrics continuously collects metrics for the SAP Host Agent. func collectHostMetrics(ctx context.Context, a any) { var params Parameters if v, ok := a.(Parameters); ok { params = v } else { log.CtxLogger(ctx).Info("Host Metrics Parameters arg is not a Parameters") return } readers := hostMetricsReaders{ configmr: &configurationmetricreader.ConfigMetricReader{OS: runtime.GOOS}, cpusr: cpustatsreader.New(runtime.GOOS, os.ReadFile, commandlineexecutor.ExecuteCommand), mmr: memorymetricreader.New(runtime.GOOS, os.ReadFile, commandlineexecutor.ExecuteCommand), dsr: diskstatsreader.New(runtime.GOOS, os.ReadFile, commandlineexecutor.ExecuteCommand), } collectTicker := time.NewTicker(60 * time.Second) heartbeatTicker := params.HeartbeatSpec.CreateTicker() defer collectTicker.Stop() defer heartbeatTicker.Stop() dailyUsageRoutineStarted.Do(func() { // LogActionDaily never returns -only sleeps for 24 and then executes the provided function. go usagemetrics.LogActionDaily(usagemetrics.CollectHostMetrics) }) // Do not wait for the first 60s tick and start collection immediately select { case <-ctx.Done(): log.CtxLogger(ctx).Info("Host metrics cancellation requested") return default: collectHostMetricsOnce(ctx, params, readers) } // Start the daemon to collect once every 60s till context is canceled for { select { case <-ctx.Done(): log.CtxLogger(ctx).Info("Host metrics cancellation requested") return case <-heartbeatTicker.C: params.HeartbeatSpec.Beat() case <-collectTicker.C: collectHostMetricsOnce(ctx, params, readers) } } } func collectHostMetricsOnce(ctx context.Context, params Parameters, readers hostMetricsReaders) { log.CtxLogger(ctx).Info("Collecting host metrics...") params.HeartbeatSpec.Beat() params.InstanceInfoReader.Read(ctx, params.Config, instanceinfo.NetworkInterfaceAddressMap) cpuStats := readers.cpusr.Read(ctx) diskStats := readers.dsr.Read(ctx, params.InstanceInfoReader.InstanceProperties()) memoryStats := readers.mmr.MemoryStats(ctx) params.AgentTime.UpdateRefreshTimes() var allMetrics []*mpb.Metric cloudMetrics := params.CloudMetricReader.Read(ctx, params.Config, params.InstanceInfoReader.InstanceProperties(), params.AgentTime) allMetrics = append(allMetrics, cloudMetrics.GetMetrics()...) osMetrics := osmetricreader.Read(cpuStats, params.InstanceInfoReader.InstanceProperties(), memoryStats, diskStats, params.AgentTime) allMetrics = append(allMetrics, osMetrics.GetMetrics()...) configMetrics := readers.configmr.Read(params.Config, cpuStats, params.InstanceInfoReader.InstanceProperties(), params.AgentTime) allMetrics = append(allMetrics, configMetrics.GetMetrics()...) metricsCollection := &mpb.MetricsCollection{Metrics: allMetrics} metricsXML = GenerateXML(metricsCollection) log.CtxLogger(ctx).Infow("Metrics collection complete", "metricscollected", len(metricsCollection.GetMetrics())) }