Skip to content

Commit d1c0810

Browse files
committed
internal/metrics: add new package for recording metrics
Move service code from sandbox/metrics.go to internal/metrics/service.go. This will enable sharing of the code for the playground front-end, allowing us to record metrics there as well. For golang/go#44822 Change-Id: I592486cdffd62dd6b9cee6cadb56ddc027788f59 Reviewed-on: https://go-review.googlesource.com/c/playground/+/302769 Trust: Alexander Rakoczy <alex@golang.org> Run-TryBot: Alexander Rakoczy <alex@golang.org> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org> Reviewed-by: Carlos Amedee <carlos@golang.org>
1 parent 6cd0363 commit d1c0810

File tree

3 files changed

+183
-156
lines changed

3 files changed

+183
-156
lines changed

internal/metrics/service.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
"errors"
11+
"fmt"
12+
"net/http"
13+
"path"
14+
"time"
15+
16+
"cloud.google.com/go/compute/metadata"
17+
"contrib.go.opencensus.io/exporter/prometheus"
18+
"contrib.go.opencensus.io/exporter/stackdriver"
19+
"go.opencensus.io/stats/view"
20+
mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
21+
)
22+
23+
// NewService initializes a *Service.
24+
//
25+
// The Service returned is configured to send metric data to
26+
// StackDriver. When not running on GCE, it will host metrics through
27+
// a prometheus HTTP handler.
28+
//
29+
// views will be passed to view.Register for export to the metric
30+
// service.
31+
func NewService(resource *MonitoredResource, views []*view.View) (*Service, error) {
32+
err := view.Register(views...)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
if !metadata.OnGCE() {
38+
view.SetReportingPeriod(5 * time.Second)
39+
pe, err := prometheus.NewExporter(prometheus.Options{})
40+
if err != nil {
41+
return nil, fmt.Errorf("prometheus.NewExporter: %w", err)
42+
}
43+
view.RegisterExporter(pe)
44+
return &Service{pExporter: pe}, nil
45+
}
46+
47+
projID, err := metadata.ProjectID()
48+
if err != nil {
49+
return nil, err
50+
}
51+
if resource == nil {
52+
return nil, errors.New("resource is required, got nil")
53+
}
54+
sde, err := stackdriver.NewExporter(stackdriver.Options{
55+
ProjectID: projID,
56+
MonitoredResource: resource,
57+
ReportingInterval: time.Minute, // Minimum interval for Stackdriver is 1 minute.
58+
})
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
// Minimum interval for Stackdriver is 1 minute.
64+
view.SetReportingPeriod(time.Minute)
65+
// Start the metrics exporter.
66+
if err := sde.StartMetricsExporter(); err != nil {
67+
return nil, err
68+
}
69+
70+
return &Service{sdExporter: sde}, nil
71+
}
72+
73+
// Service controls metric exporters.
74+
type Service struct {
75+
sdExporter *stackdriver.Exporter
76+
pExporter *prometheus.Exporter
77+
}
78+
79+
func (m *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
80+
if m.pExporter != nil {
81+
m.pExporter.ServeHTTP(w, r)
82+
return
83+
}
84+
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
85+
}
86+
87+
// Stop flushes metrics and stops exporting. Stop should be called
88+
// before exiting.
89+
func (m *Service) Stop() {
90+
if sde := m.sdExporter; sde != nil {
91+
// Flush any unsent data before exiting.
92+
sde.Flush()
93+
94+
sde.StopMetricsExporter()
95+
}
96+
}
97+
98+
// MonitoredResource wraps a *mrpb.MonitoredResource to implement the
99+
// monitoredresource.MonitoredResource interface.
100+
type MonitoredResource mrpb.MonitoredResource
101+
102+
func (r *MonitoredResource) MonitoredResource() (resType string, labels map[string]string) {
103+
return r.Type, r.Labels
104+
}
105+
106+
// GCEResource populates a MonitoredResource with GCE Metadata.
107+
//
108+
// The returned MonitoredResource will have the type set to "generic_task".
109+
func GCEResource(jobName string) (*MonitoredResource, error) {
110+
projID, err := metadata.ProjectID()
111+
if err != nil {
112+
return nil, err
113+
}
114+
zone, err := metadata.Zone()
115+
if err != nil {
116+
return nil, err
117+
}
118+
inst, err := metadata.InstanceName()
119+
if err != nil {
120+
return nil, err
121+
}
122+
group, err := instanceGroupName()
123+
if err != nil {
124+
return nil, err
125+
} else if group == "" {
126+
group = projID
127+
}
128+
129+
return (*MonitoredResource)(&mrpb.MonitoredResource{
130+
Type: "generic_task", // See: https://cloud.google.com/monitoring/api/resources#tag_generic_task
131+
Labels: map[string]string{
132+
"project_id": projID,
133+
"location": zone,
134+
"namespace": group,
135+
"job": jobName,
136+
"task_id": inst,
137+
},
138+
}), nil
139+
}
140+
141+
// instanceGroupName fetches the instanceGroupName from the instance
142+
// metadata.
143+
//
144+
// The instance group manager applies a custom "created-by" attribute
145+
// to the instance, which is not part of the metadata package API, and
146+
// must be queried separately.
147+
//
148+
// An empty string will be returned if a metadata.NotDefinedError is
149+
// returned when fetching metadata. An error will be returned if other
150+
// errors occur when fetching metadata.
151+
func instanceGroupName() (string, error) {
152+
ig, err := metadata.InstanceAttributeValue("created-by")
153+
if errors.As(err, new(metadata.NotDefinedError)) {
154+
return "", nil
155+
} else if err != nil {
156+
return "", err
157+
}
158+
// "created-by" format: "projects/{{InstanceID}}/zones/{{Zone}}/instanceGroupManagers/{{Instance Group Name}}
159+
ig = path.Base(ig)
160+
return ig, nil
161+
}

sandbox/metrics.go

Lines changed: 14 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,10 @@
55
package main
66

77
import (
8-
"errors"
9-
"fmt"
10-
"net/http"
11-
"path"
12-
"time"
13-
14-
"cloud.google.com/go/compute/metadata"
15-
"contrib.go.opencensus.io/exporter/prometheus"
16-
"contrib.go.opencensus.io/exporter/stackdriver"
178
"go.opencensus.io/plugin/ochttp"
189
"go.opencensus.io/stats"
1910
"go.opencensus.io/stats/view"
2011
"go.opencensus.io/tag"
21-
mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
2212
)
2313

2414
var (
@@ -112,148 +102,18 @@ var (
112102
}
113103
)
114104

115-
// newMetricService initializes a *metricService.
116-
//
117-
// The metricService returned is configured to send metric data to StackDriver.
118-
// When the sandbox is not running on GCE, it will host metrics through a prometheus HTTP handler.
119-
func newMetricService() (*metricService, error) {
120-
err := view.Register(
121-
containerCount,
122-
unwantedContainerCount,
123-
maxContainerCount,
124-
containerCreateCount,
125-
containerCreationLatency,
126-
ServerRequestCountView,
127-
ServerRequestBytesView,
128-
ServerResponseBytesView,
129-
ServerLatencyView,
130-
ServerRequestCountByMethod,
131-
ServerResponseCountByStatusCode)
132-
if err != nil {
133-
return nil, err
134-
}
135-
136-
if !metadata.OnGCE() {
137-
view.SetReportingPeriod(5 * time.Second)
138-
pe, err := prometheus.NewExporter(prometheus.Options{})
139-
if err != nil {
140-
return nil, fmt.Errorf("newMetricsService(): prometheus.NewExporter: %w", err)
141-
}
142-
view.RegisterExporter(pe)
143-
return &metricService{pExporter: pe}, nil
144-
}
145-
146-
projID, err := metadata.ProjectID()
147-
if err != nil {
148-
return nil, err
149-
}
150-
gr, err := gceResource("go-playground-sandbox")
151-
if err != nil {
152-
return nil, err
153-
}
154-
155-
sd, err := stackdriver.NewExporter(stackdriver.Options{
156-
ProjectID: projID,
157-
MonitoredResource: gr,
158-
ReportingInterval: time.Minute, // Minimum interval for stackdriver is 1 minute.
159-
})
160-
if err != nil {
161-
return nil, err
162-
}
163-
164-
// Minimum interval for stackdriver is 1 minute.
165-
view.SetReportingPeriod(time.Minute)
166-
// Start the metrics exporter.
167-
if err := sd.StartMetricsExporter(); err != nil {
168-
return nil, err
169-
}
170-
171-
return &metricService{sdExporter: sd}, nil
172-
}
173-
174-
// metricService controls metric exporters.
175-
type metricService struct {
176-
sdExporter *stackdriver.Exporter
177-
pExporter *prometheus.Exporter
178-
}
179-
180-
func (m *metricService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
181-
if m.pExporter != nil {
182-
m.pExporter.ServeHTTP(w, r)
183-
return
184-
}
185-
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
186-
}
187-
188-
// Stop flushes metrics and stops exporting. Stop should be called before exiting.
189-
func (m *metricService) Stop() {
190-
if sde := m.sdExporter; sde != nil {
191-
// Flush any unsent data before exiting.
192-
sde.Flush()
193-
194-
sde.StopMetricsExporter()
195-
}
196-
}
197-
198-
// monitoredResource wraps a *mrpb.MonitoredResource to implement the
199-
// monitoredresource.MonitoredResource interface.
200-
type monitoredResource mrpb.MonitoredResource
201-
202-
func (r *monitoredResource) MonitoredResource() (resType string, labels map[string]string) {
203-
return r.Type, r.Labels
204-
}
205-
206-
// gceResource populates a monitoredResource with GCE Metadata.
207-
//
208-
// The returned monitoredResource will have the type set to "generic_task".
209-
func gceResource(jobName string) (*monitoredResource, error) {
210-
projID, err := metadata.ProjectID()
211-
if err != nil {
212-
return nil, err
213-
}
214-
zone, err := metadata.Zone()
215-
if err != nil {
216-
return nil, err
217-
}
218-
iname, err := metadata.InstanceName()
219-
if err != nil {
220-
return nil, err
221-
}
222-
igName, err := instanceGroupName()
223-
if err != nil {
224-
return nil, err
225-
} else if igName == "" {
226-
igName = projID
227-
}
228-
229-
return (*monitoredResource)(&mrpb.MonitoredResource{
230-
Type: "generic_task", // See: https://cloud.google.com/monitoring/api/resources#tag_generic_task
231-
Labels: map[string]string{
232-
"project_id": projID,
233-
"location": zone,
234-
"namespace": igName,
235-
"job": jobName,
236-
"task_id": iname,
237-
},
238-
}), nil
239-
}
240-
241-
// instanceGroupName fetches the instanceGroupName from the instance metadata.
242-
//
243-
// The instance group manager applies a custom "created-by" attribute to the instance, which is not part of the
244-
// metadata package API, and must be queried separately.
245-
//
246-
// An empty string will be returned if a metadata.NotDefinedError is returned when fetching metadata.
247-
// An error will be returned if other errors occur when fetching metadata.
248-
func instanceGroupName() (string, error) {
249-
ig, err := metadata.InstanceAttributeValue("created-by")
250-
if nde := metadata.NotDefinedError(""); err != nil && !errors.As(err, &nde) {
251-
return "", err
252-
}
253-
if ig == "" {
254-
return "", nil
255-
}
256-
// "created-by" format: "projects/{{InstanceID}}/zones/{{Zone}}/instanceGroupManagers/{{Instance Group Name}}
257-
ig = path.Base(ig)
258-
return ig, err
105+
// views should contain all measurements. All *view.View added to this
106+
// slice will be registered and exported to the metric service.
107+
var views = []*view.View{
108+
containerCount,
109+
unwantedContainerCount,
110+
maxContainerCount,
111+
containerCreateCount,
112+
containerCreationLatency,
113+
ServerRequestCountView,
114+
ServerRequestBytesView,
115+
ServerResponseBytesView,
116+
ServerLatencyView,
117+
ServerRequestCountByMethod,
118+
ServerResponseCountByStatusCode,
259119
}

sandbox/sandbox.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ import (
3131
"syscall"
3232
"time"
3333

34+
"cloud.google.com/go/compute/metadata"
3435
"go.opencensus.io/plugin/ochttp"
3536
"go.opencensus.io/stats"
3637
"go.opencensus.io/tag"
3738
"go.opencensus.io/trace"
3839
"golang.org/x/playground/internal"
40+
"golang.org/x/playground/internal/metrics"
3941
"golang.org/x/playground/sandbox/sandboxtypes"
4042
)
4143

@@ -123,8 +125,12 @@ func main() {
123125

124126
mux := http.NewServeMux()
125127

126-
if ms, err := newMetricService(); err != nil {
127-
log.Printf("Failed to initialize metrics: newMetricService() = _, %v, wanted no error", err)
128+
gr, err := metrics.GCEResource("go-playground-sandbox")
129+
if err != nil && metadata.OnGCE() {
130+
log.Printf("metrics.GceService(%q) = _, %v, wanted no error.", "go-playground-sandbox", err)
131+
}
132+
if ms, err := metrics.NewService(gr, views); err != nil {
133+
log.Printf("Failed to initialize metrics: metrics.NewService() = _, %v, wanted no error", err)
128134
} else {
129135
mux.Handle("/statusz", ochttp.WithRouteTag(ms, "/statusz"))
130136
defer ms.Stop()

0 commit comments

Comments
 (0)