golang.org/x/playground@v0.0.0-20230418134305-14ebe15bcd59/internal/metrics/service.go (about)

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package metrics provides a service for reporting metrics to
     6  // Stackdriver, or locally during development.
     7  package metrics
     8  
     9  import (
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"net/http"
    14  	"path"
    15  	"time"
    16  
    17  	"cloud.google.com/go/compute/metadata"
    18  	"contrib.go.opencensus.io/exporter/prometheus"
    19  	"contrib.go.opencensus.io/exporter/stackdriver"
    20  	"go.opencensus.io/stats/view"
    21  	"google.golang.org/appengine"
    22  	mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
    23  )
    24  
    25  // NewService initializes a *Service.
    26  //
    27  // The Service returned is configured to send metric data to
    28  // StackDriver. When not running on GCE, it will host metrics through
    29  // a prometheus HTTP handler.
    30  //
    31  // views will be passed to view.Register for export to the metric
    32  // service.
    33  func NewService(resource *MonitoredResource, views []*view.View) (*Service, error) {
    34  	err := view.Register(views...)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	if !metadata.OnGCE() {
    40  		view.SetReportingPeriod(5 * time.Second)
    41  		pe, err := prometheus.NewExporter(prometheus.Options{})
    42  		if err != nil {
    43  			return nil, fmt.Errorf("prometheus.NewExporter: %w", err)
    44  		}
    45  		view.RegisterExporter(pe)
    46  		return &Service{pExporter: pe}, nil
    47  	}
    48  
    49  	projID, err := metadata.ProjectID()
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	if resource == nil {
    54  		return nil, errors.New("resource is required, got nil")
    55  	}
    56  	sde, err := stackdriver.NewExporter(stackdriver.Options{
    57  		ProjectID:         projID,
    58  		MonitoredResource: resource,
    59  		ReportingInterval: time.Minute, // Minimum interval for Stackdriver is 1 minute.
    60  	})
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	// Minimum interval for Stackdriver is 1 minute.
    66  	view.SetReportingPeriod(time.Minute)
    67  	// Start the metrics exporter.
    68  	if err := sde.StartMetricsExporter(); err != nil {
    69  		sde.Close()
    70  		return nil, err
    71  	}
    72  
    73  	return &Service{sdExporter: sde}, nil
    74  }
    75  
    76  // Service controls metric exporters.
    77  type Service struct {
    78  	sdExporter *stackdriver.Exporter
    79  	pExporter  *prometheus.Exporter
    80  }
    81  
    82  func (m *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    83  	if m.pExporter != nil {
    84  		m.pExporter.ServeHTTP(w, r)
    85  		return
    86  	}
    87  	http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
    88  }
    89  
    90  // Stop flushes metrics and stops exporting. Stop should be called
    91  // before exiting.
    92  func (m *Service) Stop() {
    93  	if sde := m.sdExporter; sde != nil {
    94  		// Flush any unsent data before exiting.
    95  		sde.Flush()
    96  
    97  		sde.StopMetricsExporter()
    98  	}
    99  }
   100  
   101  // MonitoredResource wraps a *mrpb.MonitoredResource to implement the
   102  // monitoredresource.MonitoredResource interface.
   103  type MonitoredResource mrpb.MonitoredResource
   104  
   105  func (r *MonitoredResource) MonitoredResource() (resType string, labels map[string]string) {
   106  	return r.Type, r.Labels
   107  }
   108  
   109  // GCEResource populates a MonitoredResource with GCE Metadata.
   110  //
   111  // The returned MonitoredResource will have the type set to "generic_task".
   112  func GCEResource(jobName string) (*MonitoredResource, error) {
   113  	projID, err := metadata.ProjectID()
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	zone, err := metadata.Zone()
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	inst, err := metadata.InstanceName()
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	group, err := instanceGroupName()
   126  	if err != nil {
   127  		return nil, err
   128  	} else if group == "" {
   129  		group = projID
   130  	}
   131  
   132  	return (*MonitoredResource)(&mrpb.MonitoredResource{
   133  		Type: "generic_task", // See: https://cloud.google.com/monitoring/api/resources#tag_generic_task
   134  		Labels: map[string]string{
   135  			"project_id": projID,
   136  			"location":   zone,
   137  			"namespace":  group,
   138  			"job":        jobName,
   139  			"task_id":    inst,
   140  		},
   141  	}), nil
   142  }
   143  
   144  // GAEResource returns a *MonitoredResource with fields populated and
   145  // for StackDriver.
   146  //
   147  // The resource will be in StackDrvier's gae_instance type.
   148  func GAEResource(ctx context.Context) (*MonitoredResource, error) {
   149  	// appengine.IsAppEngine is confusingly false as we're using a custom
   150  	// container and building without the appenginevm build constraint.
   151  	// Check metadata.OnGCE instead.
   152  	if !metadata.OnGCE() {
   153  		return nil, fmt.Errorf("not running on appengine")
   154  	}
   155  	projID, err := metadata.ProjectID()
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	return (*MonitoredResource)(&mrpb.MonitoredResource{
   160  		Type: "gae_instance",
   161  		Labels: map[string]string{
   162  			"project_id":  projID,
   163  			"module_id":   appengine.ModuleName(ctx),
   164  			"version_id":  appengine.VersionID(ctx),
   165  			"instance_id": appengine.InstanceID(),
   166  			"location":    appengine.Datacenter(ctx),
   167  		},
   168  	}), nil
   169  }
   170  
   171  // instanceGroupName fetches the instanceGroupName from the instance
   172  // metadata.
   173  //
   174  // The instance group manager applies a custom "created-by" attribute
   175  // to the instance, which is not part of the metadata package API, and
   176  // must be queried separately.
   177  //
   178  // An empty string will be returned if a metadata.NotDefinedError is
   179  // returned when fetching metadata. An error will be returned if other
   180  // errors occur when fetching metadata.
   181  func instanceGroupName() (string, error) {
   182  	ig, err := metadata.InstanceAttributeValue("created-by")
   183  	if errors.As(err, new(metadata.NotDefinedError)) {
   184  		return "", nil
   185  	} else if err != nil {
   186  		return "", err
   187  	}
   188  	// "created-by" format: "projects/{{InstanceID}}/zones/{{Zone}}/instanceGroupManagers/{{Instance Group Name}}
   189  	ig = path.Base(ig)
   190  	return ig, nil
   191  }