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()))
}