blob: a19aa268291907ab33cd98452e25b10a543b158d [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package metrics provides a service for reporting metrics to
// Stackdriver, or locally during development.
package metrics
import (
"context"
"errors"
"fmt"
"net/http"
"path"
"time"
"cloud.google.com/go/compute/metadata"
"contrib.go.opencensus.io/exporter/prometheus"
"contrib.go.opencensus.io/exporter/stackdriver"
"go.opencensus.io/stats/view"
"google.golang.org/appengine"
mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
)
// NewService initializes a *Service.
//
// The Service returned is configured to send metric data to
// StackDriver. When not running on GCE, it will host metrics through
// a prometheus HTTP handler.
//
// views will be passed to view.Register for export to the metric
// service.
func NewService(resource *MonitoredResource, views []*view.View) (*Service, error) {
err := view.Register(views...)
if err != nil {
return nil, err
}
if !metadata.OnGCE() {
view.SetReportingPeriod(5 * time.Second)
pe, err := prometheus.NewExporter(prometheus.Options{})
if err != nil {
return nil, fmt.Errorf("prometheus.NewExporter: %w", err)
}
view.RegisterExporter(pe)
return &Service{pExporter: pe}, nil
}
projID, err := metadata.ProjectID()
if err != nil {
return nil, err
}
if resource == nil {
return nil, errors.New("resource is required, got nil")
}
sde, err := stackdriver.NewExporter(stackdriver.Options{
ProjectID: projID,
MonitoredResource: resource,
ReportingInterval: time.Minute, // Minimum interval for Stackdriver is 1 minute.
})
if err != nil {
return nil, err
}
// Minimum interval for Stackdriver is 1 minute.
view.SetReportingPeriod(time.Minute)
// Start the metrics exporter.
if err := sde.StartMetricsExporter(); err != nil {
sde.Close()
return nil, err
}
return &Service{sdExporter: sde}, nil
}
// Service controls metric exporters.
type Service struct {
sdExporter *stackdriver.Exporter
pExporter *prometheus.Exporter
}
func (m *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if m.pExporter != nil {
m.pExporter.ServeHTTP(w, r)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
// Stop flushes metrics and stops exporting. Stop should be called
// before exiting.
func (m *Service) Stop() {
if sde := m.sdExporter; sde != nil {
// Flush any unsent data before exiting.
sde.Flush()
sde.StopMetricsExporter()
}
}
// MonitoredResource wraps a *mrpb.MonitoredResource to implement the
// monitoredresource.MonitoredResource interface.
type MonitoredResource mrpb.MonitoredResource
func (r *MonitoredResource) MonitoredResource() (resType string, labels map[string]string) {
return r.Type, r.Labels
}
// GCEResource populates a MonitoredResource with GCE Metadata.
//
// The returned MonitoredResource will have the type set to "generic_task".
func GCEResource(jobName string) (*MonitoredResource, error) {
projID, err := metadata.ProjectID()
if err != nil {
return nil, err
}
zone, err := metadata.Zone()
if err != nil {
return nil, err
}
inst, err := metadata.InstanceName()
if err != nil {
return nil, err
}
group, err := instanceGroupName()
if err != nil {
return nil, err
} else if group == "" {
group = projID
}
return (*MonitoredResource)(&mrpb.MonitoredResource{
Type: "generic_task", // See: https://cloud.google.com/monitoring/api/resources#tag_generic_task
Labels: map[string]string{
"project_id": projID,
"location": zone,
"namespace": group,
"job": jobName,
"task_id": inst,
},
}), nil
}
// GAEResource returns a *MonitoredResource with fields populated and
// for StackDriver.
//
// The resource will be in StackDrvier's gae_instance type.
func GAEResource(ctx context.Context) (*MonitoredResource, error) {
// appengine.IsAppEngine is confusingly false as we're using a custom
// container and building without the appenginevm build constraint.
// Check metadata.OnGCE instead.
if !metadata.OnGCE() {
return nil, fmt.Errorf("not running on appengine")
}
projID, err := metadata.ProjectID()
if err != nil {
return nil, err
}
return (*MonitoredResource)(&mrpb.MonitoredResource{
Type: "gae_instance",
Labels: map[string]string{
"project_id": projID,
"module_id": appengine.ModuleName(ctx),
"version_id": appengine.VersionID(ctx),
"instance_id": appengine.InstanceID(),
"location": appengine.Datacenter(ctx),
},
}), nil
}
// instanceGroupName fetches the instanceGroupName from the instance
// metadata.
//
// The instance group manager applies a custom "created-by" attribute
// to the instance, which is not part of the metadata package API, and
// must be queried separately.
//
// An empty string will be returned if a metadata.NotDefinedError is
// returned when fetching metadata. An error will be returned if other
// errors occur when fetching metadata.
func instanceGroupName() (string, error) {
ig, err := metadata.InstanceAttributeValue("created-by")
if errors.As(err, new(metadata.NotDefinedError)) {
return "", nil
} else if err != nil {
return "", err
}
// "created-by" format: "projects/{{InstanceID}}/zones/{{Zone}}/instanceGroupManagers/{{Instance Group Name}}
ig = path.Base(ig)
return ig, nil
}