Skip to content

Commit

Permalink
internal/gcp/metrics: metric support for GCP
Browse files Browse the repository at this point in the history
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
jba committed Aug 19, 2024
1 parent 92576c5 commit ff61e20
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 6 deletions.
1 change: 1 addition & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtm
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/tdewolff/minify/v2 v2.12.4/go.mod h1:h+SRvSIX3kwgwTFOpSckvSxgax3uy8kZTSF1Ojrr3bk=
github.com/tdewolff/parse/v2 v2.6.4/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
Expand Down
30 changes: 27 additions & 3 deletions internal/gaby/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"golang.org/x/oscar/internal/gcp/firestore"
"golang.org/x/oscar/internal/gcp/gcphandler"
"golang.org/x/oscar/internal/gcp/gcpsecret"
"golang.org/x/oscar/internal/gcp/metrics"
"golang.org/x/oscar/internal/gemini"
"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/githubdocs"
Expand Down Expand Up @@ -86,7 +87,8 @@ func main() {
addr: "localhost:4229", // 4229 = gaby on a phone
}

g.initGCP()
shutdown := g.initGCP()
defer shutdown()

var syncs, changes []func(context.Context)

Expand Down Expand Up @@ -168,8 +170,8 @@ func (g *Gaby) initLocal() {
g.vector = storage.MemVectorDB(db, g.slog, "")
}

// initGCP initializes a Gaby instance to use GCP databases.
func (g *Gaby) initGCP() {
// initGCP initializes a Gaby instance to use GCP databases and other resources.
func (g *Gaby) initGCP() (shutdown func()) {
if flags.project == "" {
projectID, err := metadata.ProjectIDWithContext(g.ctx)
if err != nil {
Expand Down Expand Up @@ -216,6 +218,12 @@ func (g *Gaby) initGCP() {
log.Fatal(err)
}
g.secret = sdb

shutdown, err = metrics.Init(g.ctx, g.slog, flags.project)
if err != nil {
log.Fatal(err)
}
return shutdown
}

// searchLoop runs an interactive search loop.
Expand All @@ -242,6 +250,8 @@ func (g *Gaby) searchLoop() {

// serveHTTP serves HTTP endpoints for Gaby.
func (g *Gaby) serveHTTP() {
cronCounter := metrics.NewCounter(metricName("crons"), "number of /cron requests")

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Gaby\n")
fmt.Fprintf(w, "meta: %+v\n", g.meta)
Expand Down Expand Up @@ -271,6 +281,7 @@ func (g *Gaby) serveHTTP() {
for _, cron := range g.crons {
cron(g.ctx)
}
cronCounter.Add(r.Context(), 1)
})

// Listen in this goroutine so that we can return a synchronous error
Expand Down Expand Up @@ -307,6 +318,19 @@ func onCloudRun() bool {
return os.Getenv("K_SERVICE") != "" && os.Getenv("K_REVISION") != ""
}

// metricName returns the full metric name for the given short name.
// The names are chosen to display nicely on the Metric Explorer's "select a metric"
// dropdown. Production metrics will group under "Gaby", while others will
// have their own, distinct groups.
func metricName(shortName string) string {
if flags.firestoredb == "prod" {
return "gaby/" + shortName
}
// Using a hyphen or slash after "gaby" puts the metric in the "Gaby" group.
// We want non-prod metrics to be in a different group.
return "gaby_" + flags.firestoredb + "/" + shortName
}

// Crawling parameters

var godevAllow = []string{
Expand Down
14 changes: 11 additions & 3 deletions internal/gcp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,29 @@ require (
cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/iam v1.1.10 // indirect
cloud.google.com/go/longrunning v0.5.11 // indirect
cloud.google.com/go/monitoring v1.20.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-replayers/grpcreplay v1.3.1-0.20240807114419-950aeedc364f // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.28.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
Expand Down
22 changes: 22 additions & 0 deletions internal/gcp/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI=
cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps=
cloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk=
cloud.google.com/go/longrunning v0.5.11/go.mod h1:rDn7//lmlfWV1Dx6IB4RatCPenTwwmqXuiP0/RgoEO4=
cloud.google.com/go/monitoring v1.20.2 h1:B/L+xrw9PYO7ywh37sgnjI/6dzEE+yQTAwfytDcpPto=
cloud.google.com/go/monitoring v1.20.2/go.mod h1:36rpg/7fdQ7NX5pG5x1FA7cXTVXusOp6Zg9r9e1+oek=
cloud.google.com/go/secretmanager v1.13.3 h1:VqUVYY3U6uFXOhPdZgAoZH9m8E6p7eK02TsDRj2SBf4=
cloud.google.com/go/secretmanager v1.13.3/go.mod h1:e45+CxK0w6GaL4hS+KabgQskl4RdSS30b+HRf0TH0kk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.0 h1:N4xzkSD2BkRwEZSPf3C2eUZxjS5trpo4gOwRh8mu+BA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.0/go.mod h1:p2puVVSKjQ84Qb1gzw2XHLs34WQyHTYFZLaVxypAFYs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
Expand Down Expand Up @@ -62,6 +70,8 @@ github.com/google/go-replayers/grpcreplay v1.3.1-0.20240807114419-950aeedc364f/g
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
Expand All @@ -79,18 +89,30 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/detectors/gcp v1.28.0 h1:eAaOyCwPqwAG7INWn0JTDD3KFR4qbSlhh0YCuFOmmDE=
go.opentelemetry.io/contrib/detectors/gcp v1.28.0/go.mod h1:9BIqH22qyHWAiZxQh0whuJygro59z+nbMVuc7ciiGug=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08=
go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
Expand Down
105 changes: 105 additions & 0 deletions internal/gcp/metrics/metrics.go
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
}
45 changes: 45 additions & 0 deletions internal/gcp/metrics/metrics_test.go
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)
}
}

0 comments on commit ff61e20

Please sign in to comment.