cmd/core_plugin/telemetry/telemetry.go (98 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 telemetry implements the scheduler for collecting and publishing
// telemetry data.
package telemetry
import (
"context"
"encoding/base64"
"fmt"
"runtime"
"time"
"github.com/GoogleCloudPlatform/google-guest-agent/cmd/core_plugin/manager"
acppb "github.com/GoogleCloudPlatform/google-guest-agent/internal/acp/proto/google_guest_agent/acp"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/metadata"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/osinfo"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/scheduler"
"google.golang.org/protobuf/proto"
)
const (
// telemetryModuleID is the module ID for telemetry scheduler.
telemetryModuleID = "telemetry-publisher"
// telemetryInterval is the interval at which telemetry data is recorded.
telemetryInterval = 24 * time.Hour
// programName is the name of the program used in telemetry data.
programName = "GCEGuestAgent"
)
// Job implements job scheduler interface for recording telemetry.
type Job struct {
// client is the MDS client.
client metadata.MDSClientInterface
// agentVersion is the current agent version.
agentVersion string
// osInfoReader is the reader for osinfo. Setting here allows unit testing.
osInfoReader func() osinfo.OSInfo
}
// NewModule returns the first boot module for late stage registration.
func NewModule(context.Context) *manager.Module {
return &manager.Module{
ID: telemetryModuleID,
Setup: moduleSetup,
Quit: teardown,
Description: "Telemetry module collects and publishes telemetry data to MDS",
}
}
// teardown unschedules the telemetry job.
func teardown(context.Context) {
scheduler.Instance().UnscheduleJob(telemetryModuleID)
}
// moduleSetup schedules a job to collect and publish telemetry data.
func moduleSetup(ctx context.Context, data any) error {
job := &Job{client: metadata.New(), osInfoReader: osinfo.Read}
return scheduler.Instance().ScheduleJob(ctx, job)
}
// ID returns the ID for this job.
func (j *Job) ID() string {
return telemetryModuleID
}
// Run records telemetry data.
func (j *Job) Run(ctx context.Context) (bool, error) {
osInfo, err := formatOSInfo(j.osInfoReader())
if err != nil {
return j.ShouldEnable(ctx), err
}
agentInfo, err := formatAgentInfo(cfg.Retrieve().Core.Version)
if err != nil {
return j.ShouldEnable(ctx), err
}
return j.ShouldEnable(ctx), j.record(ctx, osInfo, agentInfo)
}
// Interval returns the interval at which job is executed.
func (j *Job) Interval() (time.Duration, bool) {
return telemetryInterval, true
}
// ShouldEnable returns true as long as DisableTelemetry is not set in metadata.
func (j *Job) ShouldEnable(ctx context.Context) bool {
md, err := j.client.Get(ctx)
if err != nil {
return false
}
return !md.Instance().Attributes().DisableTelemetry() && !md.Project().Attributes().DisableTelemetry()
}
// record records telemetry data.
func (j *Job) record(ctx context.Context, osinfo, agentInfo string) error {
headers := map[string]string{
"X-Google-Guest-Agent": agentInfo,
"X-Google-Guest-OS": osinfo,
}
// We don't care about any return value, all we need to do is make some call
// with the telemetry headers.
_, err := j.client.GetKey(ctx, "", headers)
return err
}
// formatAgentInfo marshals agent info in required proto and returns in base64
// encoded form.
func formatAgentInfo(version string) (string, error) {
data, err := proto.Marshal(&acppb.AgentInfo{
Name: programName,
Architecture: runtime.GOARCH,
Version: version,
})
if err != nil {
return "", fmt.Errorf("error marshalling AgentInfo: %w", err)
}
return base64.StdEncoding.EncodeToString(data), nil
}
// formatOSInfo marshals osinfo in required proto and returns in base64 encoded
// form.
func formatOSInfo(os osinfo.OSInfo) (string, error) {
data, err := proto.Marshal(&acppb.OSInfo{
Architecture: os.Architecture,
Type: runtime.GOOS,
Version: os.VersionID,
ShortName: os.OS,
LongName: os.PrettyName,
KernelRelease: os.KernelRelease,
KernelVersion: os.KernelVersion,
})
if err != nil {
return "", fmt.Errorf("error marshalling OSInfo: %w", err)
}
return base64.StdEncoding.EncodeToString(data), nil
}