-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
internal/gcp/metrics: metric support for GCP
Add a package that sets up and wraps Open Telemetry metrics. Define one metric, a counter for the number of cron requests. It's not very interesting but useful to test that metrics are being exported properly. For #3. Change-Id: Ia99b618e97df25af10bdda3027fb0bce2bdcf701 Reviewed-on: https://go-review.googlesource.com/c/oscar/+/606815 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Tatiana Bradley <[email protected]>
- Loading branch information
Showing
6 changed files
with
211 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
// Copyright 2024 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 supports gathering and publishing metrics | ||
// on GCP using OpenTelemetry. | ||
package metrics | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"log/slog" | ||
"strings" | ||
"sync/atomic" | ||
|
||
gcpexporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric" | ||
"go.opentelemetry.io/contrib/detectors/gcp" | ||
ometric "go.opentelemetry.io/otel/metric" | ||
sdkmetric "go.opentelemetry.io/otel/sdk/metric" | ||
"go.opentelemetry.io/otel/sdk/metric/metricdata" | ||
"go.opentelemetry.io/otel/sdk/resource" | ||
) | ||
|
||
// The meter for creating metric instruments (counters and so on). | ||
var meter ometric.Meter | ||
var logger *slog.Logger | ||
|
||
func Init(ctx context.Context, lg *slog.Logger, projectID string) (shutdown func(), err error) { | ||
// Create an exporter to send metrics to the GCP Monitoring service. | ||
ex, err := gcpexporter.New(gcpexporter.WithProjectID(projectID)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
// Wrap it with logging so we can see in the logs that data is being sent. | ||
lex := &loggingExporter{lg, ex} | ||
// By default, the PeriodicReader will export metrics once per minute. | ||
r := sdkmetric.NewPeriodicReader(lex) | ||
|
||
// Construct a Resource, which identifies the source of the metrics to GCP. | ||
// Although Cloud Run has its own resource type, user-defined metrics cannot use it. | ||
// Instead the gcp detector will put our metrics in the Generic Task group. | ||
// It doesn't really matter, as long as you know where to look. | ||
res, err := resource.New(ctx, resource.WithDetectors(gcp.NewDetector())) | ||
if errors.Is(err, resource.ErrPartialResource) || errors.Is(err, resource.ErrSchemaURLConflict) { | ||
lg.Warn("resource.New non-fatal error", "err", err) | ||
} else if err != nil { | ||
return nil, err | ||
} | ||
lg.Info("creating OTel MeterProvider", "resource", res.String()) | ||
mp := sdkmetric.NewMeterProvider( | ||
sdkmetric.WithResource(res), | ||
sdkmetric.WithReader(r), | ||
) | ||
logger = lg | ||
meter = mp.Meter("gcp") | ||
return func() { | ||
if err := mp.Shutdown(context.Background()); err != nil { | ||
lg.Warn("metric shutdown failed", "err", err) | ||
} | ||
}, nil | ||
} | ||
|
||
// NewCounter creates an integer counter instrument. | ||
// It panics if the counter cannot be created. | ||
func NewCounter(name, description string) ometric.Int64Counter { | ||
c, err := meter.Int64Counter(name, ometric.WithDescription(description)) | ||
if err != nil { | ||
logger.Error("counter creation failed", "name", name) | ||
panic(err) | ||
} | ||
return c | ||
} | ||
|
||
// A loggingExporter wraps an [sdkmetric.Exporter] with logging. | ||
type loggingExporter struct { | ||
lg *slog.Logger | ||
sdkmetric.Exporter | ||
} | ||
|
||
// For testing. | ||
var totalExports, failedExports atomic.Int64 | ||
|
||
func (e *loggingExporter) Export(ctx context.Context, rm *metricdata.ResourceMetrics) error { | ||
var b strings.Builder | ||
for _, sm := range rm.ScopeMetrics { | ||
fmt.Fprintf(&b, "scope=%+v", sm.Scope) | ||
for _, m := range sm.Metrics { | ||
fmt.Fprintf(&b, " %q", m.Name) | ||
} | ||
} | ||
e.lg.Debug("start metric export", | ||
"resource", rm.Resource.String(), | ||
"metrics", b.String(), | ||
) | ||
err := e.Exporter.Export(ctx, rm) | ||
totalExports.Add(1) | ||
if err != nil { | ||
e.lg.Warn("metric export failed", "err", err) | ||
failedExports.Add(1) | ||
} else { | ||
e.lg.Debug("end metric export") | ||
} | ||
return err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// Copyright 2024 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 | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"testing" | ||
|
||
"golang.org/x/oscar/internal/testutil" | ||
) | ||
|
||
var project = flag.String("project", "", "GCP project ID") | ||
|
||
// This test checks that metrics are exported to the GCP monitoring API. | ||
// (If we don't actually send the metrics, there is really nothing to test.) | ||
func Test(t *testing.T) { | ||
if *project == "" { | ||
t.Skip("skipping without -project") | ||
} | ||
ctx := context.Background() | ||
|
||
shutdown, err := Init(ctx, testutil.Slogger(t), *project) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
c := NewCounter("test-counter", "a counter for testing") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
c.Add(ctx, 1) | ||
|
||
// Force an export even if the interval hasn't passed. | ||
shutdown() | ||
|
||
if g, w := totalExports.Load(), int64(1); g != w { | ||
t.Errorf("total exports: got %d, want %d", g, w) | ||
} | ||
|
||
if g, w := failedExports.Load(), int64(0); g != w { | ||
t.Errorf("failed exports: got %d, want %d", g, w) | ||
} | ||
} |