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 }