diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index 1e1460e1..4eb7ea19 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -45,10 +45,10 @@ jobs:
echo "run setup script"
./scripts/setup
echo "run server"
- ./bin/forecaster -monitoring-enabled=false &
- ./bin/locator -monitoring-enabled=false &
- ./bin/tester -monitoring-enabled=false &
- ./bin/front -monitoring-enabled=false &
+ ./bin/forecaster &
+ ./bin/locator &
+ ./bin/tester &
+ ./bin/front &
check_service_health "http://localhost:8081/healthz" &
check_service_health "http://localhost:8083/healthz" &
check_service_health "http://localhost:8091/healthz" &
diff --git a/.gitignore b/.gitignore
index ddb646d8..738e2d8f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,8 @@
.vscode/
-example/weather/services/forecaster/cmd/forecaster/forecaster
-example/weather/services/locator/cmd/locator/locator
-example/weather/services/front/cmd/front/front
example/weather/.overmind.sock
example/weather/bin/*
+example/weather/services/forecaster/cmd/forecaster/forecaster
+example/weather/services/front/cmd/front/front
+example/weather/services/locator/cmd/locator/locator
+example/weather/signoz
coverage.out
-go.work
diff --git a/LICENSE b/LICENSE
index d0e57815..8fbd1ce6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2022 Raphael Simon and Clue Contributors
+Copyright (c) 2024 Raphael Simon and Clue Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index f1a2d546..e7722a1b 100644
--- a/README.md
+++ b/README.md
@@ -1,38 +1,403 @@
+
+
# clue: Microservice Instrumentation
-[![Build Status](https://github.com/goadesign/clue/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/goadesign/clue/actions?query=branch%3Amain+event%3Apush)
-[![codecov](https://codecov.io/gh/goadesign/clue/branch/main/graph/badge.svg?token=HVP4WT1PS6)](https://codecov.io/gh/goadesign/clue)
-[![Go Report Card](https://goreportcard.com/badge/goa.design/clue)](https://goreportcard.com/report/goa.design/clue)
-[![Go Reference](https://pkg.go.dev/badge/goa.design/clue.svg)](https://pkg.go.dev/goa.design/clue)
+[![Go Reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=for-the-badge)](https://pkg.go.dev/goa.design/clue)
+[![License](https://img.shields.io/badge/License-MIT%202.0-blue?style=for-the-badge)](LICENSE)
+
+[![Build Status](https://img.shields.io/github/actions/workflow/status/goadesign/clue/ci.yaml?style=for-the-badge)](https://github.com/goadesign/clue/actions?query=branch%3Amain+event%3Apush)
+[![codecov](https://img.shields.io/codecov/c/github/goadesign/clue/main?style=for-the-badge&token=HVP4WT1PS6)](https://codecov.io/gh/goadesign/clue)
+[![Go Report Card](https://img.shields.io/badge/go%20report-A+-brightgreen.svg?style=for-the-badge)](https://goreportcard.com/report/goa.design/clue)
+
+
## Overview
-`clue` provides a set of Go packages for instrumenting microservices. The
-emphasis is on simplicity and ease of use. Although not a requirement, `clue`
+Clue provides a set of Go packages for instrumenting microservices. The
+emphasis is on simplicity and ease of use. Although not a requirement, Clue
works best when used in microservices written using
[Goa](https://github.com/goadesign/goa).
-`clue` covers the following topics:
+Clue covers the following topics:
+* Instrumentation: the [clue](clue/) package configures OpenTelemetry
+ for service instrumentation.
* Logging: the [log](log/) package provides a context-based logging API that
intelligently selects what to log.
-* Metrics: the [metrics](metrics/) package makes it possible for
- services to expose a Prometheus compatible `/metrics` HTTP endpoint.
* Health checks: the [health](health/) package provides a simple way for
services to expose a health check endpoint.
* Dependency mocks: the [mock](mock/) package provides a way to mock
downstream dependencies for testing.
-* Tracing: the [trace](trace/) package conforms to the
- [OpenTelemetry](https://opentelemetry.io/) specification to trace requests.
* Debugging: the [debug](debug/) package makes it possible to troubleshoot
and profile services at runtime.
-The [weather](example/weather) example illustrates how to use `clue` to
-instrument a system of Goa microservices. The example comes with a set of
-scripts that can be used to compile and start the system as well as a complete
-Grafana stack to query metrics and traces. See the
+Clue's goal is to provide all the anciallary functionality required to efficiently
+operate a microservice style architecture including instrumentation, logging,
+debugging and health checks. Clue is not a framework and does not provide
+functionality that is already available in the standard library or in other
+packages. For example, Clue does not provide a HTTP router or a HTTP server
+implementation. Instead, Clue provides a way to instrument existing HTTP or gRPC
+servers and clients using the standard library and the OpenTelemetry API.
+
+## Packages
+
+* The `clue` package provides a simple API for configuring OpenTelemetry
+ instrumentation. The package also provides a way to configure the log
+ package to automatically annotate log messages with trace and span IDs.
+ The package also implements a dynamic trace sampler that can be used to
+ sample traces based on a target maximum number of traces per second.
+
+* The `log` package offers a streamlined, context-based structured logger that
+ efficiently buffers log messages. It smartly determines the optimal time to
+ flush these messages to the underlying logger. In its default configuration,
+ the log package flushes logs upon the logging of an error or when a request is
+ traced. This design choice minimizes logging overhead for untraced requests,
+ ensuring efficient logging operations.
+
+* The `health` package provides a simple way to expose a health check endpoint
+ that can be used by orchestration systems such as Kubernetes to determine
+ whether a service is ready to receive traffic. The package implements the
+ concept of checkers that can be used to implement custom health checks with
+ a default implementation that relies on the ability to ping downstream
+ dependencies.
+
+* The `mock` package provides a way to mock downstream dependencies for testing.
+ The package provides a tool that generates mock implementations of interfaces
+ and a way to configure the generated mocks to validate incoming payloads and
+ return canned responses.
+
+* The `debug` package provides a way to dynamically control the log level of a
+ running service. The package also provides a way to expose the pprof Go
+ profiling endpoints and a way to expose the log level control endpoint.
+
+## Installation
+
+Clue requires Go 1.21 or later. Install the packages required for your
+application using `go get`, for example:
+
+```bash
+go get goa.design/clue/clue
+go get goa.design/clue/log
+go get goa.design/clue/health
+go get goa.design/clue/mock
+go get goa.design/clue/debug
+```
+
+## Usage
+
+The following snippet illustrates how to use `clue` to instrument a HTTP server:
+
+```go
+ctx := log.Context(context.Background(), // Create a clue logger context.
+ log.WithFunc(log.Span)) // Log trace and span IDs.
+
+metricExporter := stdoutmetric.New() // Create metric and span exporters.
+spanExporter := stdouttrace.New()
+cfg := clue.NewConfig(ctx, "service", "1.0.0", metricExporter, spanExporter)
+clue.ConfigureOpenTelemetry(ctx, cfg) // Configure OpenTelemetry.
+
+handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("Hello, World!"))
+}) // Create HTTP handler.
+handler = otelhttp.NewHandler(handler, "service" ) // Instrument handler.
+handler = debug.HTTP()(handler) // Setup debug log level control.
+handler = log.HTTP(ctx)(handler) // Add logger to request context and log requests.
+
+mux := http.NewServeMux() // Create HTTP mux.
+mux.HandleFunc("/", handler) // Mount handler.
+debug.MountDebugLogEnabler(mux) // Mount debug log level control handler.
+debug.MountDebugPprof(mux) // Mount pprof handlers.
+mux.HandleFunc("/health",
+ health.NewHandler(health.NewChecker())) // Mount health check handler.
+
+http.ListenAndServe(":8080", mux) // Start HTTP server.
+```
+
+Similarly, the following snippet illustrates how to instrument a gRPC server:
+
+```go
+ctx := log.Context(context.Background(), // Create a clue logger context.
+ log.WithFunc(log.Span)) // Log trace and span IDs.
+
+metricExporter := stdoutmetric.New()
+spanExporter := stdouttrace.New()
+cfg := clue.NewConfig(ctx, "service", "1.0.0", metricExporter, spanExporter)
+clue.ConfigureOpenTelemetry(ctx, cfg) // Configure OpenTelemetry.
+
+svr := grpc.NewServer(
+ grpc.ChainUnaryInterceptor(
+ log.UnaryServerInterceptor(ctx), // Add logger to request context and log requests.
+ debug.UnaryServerInterceptor()), // Enable debug log level control
+ grpc.StatsHandler(otelgrpc.NewServerHandler()), // Instrument server.
+)
+```
+
+Note that in the case of gRPC, a separate HTTP server is required to expose the
+debug log level control, pprof and health check endpoints:
+
+```go
+mux := http.NewServeMux() // Create HTTP mux.
+debug.MountDebugLogEnabler(mux) // Mount debug log level control handler.
+debug.MountDebugPprof(mux) // Mount pprof handlers.
+mux.HandleFunc("/health",
+ health.NewHandler(health.NewChecker())) // Mount health check handler.
+
+go http.ListenAndServe(":8081", mux) // Start HTTP server.
+```
+
+## Exporters
+
+Exporter are responsible for exporting telemetry data to a backend. The
+[OpenTelemetry Exporters documentation](https://opentelemetry.io/docs/instrumentation/go/exporters/)
+provides a list of available exporters.
+
+For example, configuring an [OTLP](https://opentelemetry.io/docs/specs/otlp/)
+compliant span exporter can be done as follows:
+
+```go
+import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+// ...
+spanExporter, err := otlptracegrpc.New(
+ context.Background(),
+ otlptracegrpc.WithEndpoint("localhost:4317"),
+ otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()))
+```
+
+While configuring an OTLP compliant metric exporters can be done as follows:
+
+```go
+import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
+// ...
+metricExporter, err := otlpmetricgrpc.New(
+ context.Background(),
+ otlpmetricgrpc.WithEndpoint("localhost:4317"),
+ otlpmetricgrpc.WithTLSCredentials(insecure.NewCredentials()))
+```
+
+These exporters can then be used to configure Clue:
+
+```go
+// Configure OpenTelemetry.
+cfg := clue.NewConfig(ctx, "service", "1.0.0", metricExporter, spanExporter)
+clue.ConfigureOpenTelemetry(ctx, cfg)
+```
+
+## Clients
+
+HTTP clients can be instrumented using the Clue `log` and OpenTelemetry `otelhttptrace` packages. The `log.Client` function wraps a HTTP transport and logs the request and response. The `otelhttptrace.Client` function wraps a HTTP transport and adds OpenTelemetry tracing to the request.
+
+```go
+import (
+ "context"
+ "net/http"
+ "net/http/httptrace"
+
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttptrace"
+ "goa.design/clue/log"
+)
+
+// ...
+httpc := &http.Client{
+ Transport: log.Client(
+ otelhttp.NewTransport(
+ http.DefaultTransport,
+ otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
+ return otelhttptrace.NewClientTrace(ctx)
+ }),
+ ),
+ ),
+}
+```
+
+Similarly, gRPC clients can be instrumented using the Clue `log` and OpenTelemtry `otelgrpc` packages.
+
+```go
+import (
+ "context"
+
+ "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
+ "goa.design/clue/log"
+)
+
+// ...
+grpcconn, err := grpc.DialContext(ctx,
+ "localhost:8080",
+ grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()),
+ grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
+)
+```
+
+## Custom Instrumentation
+
+Clue relies on the OpenTelemetry API for creating custom instrumentation. The
+following snippet illustrates how to create a custom counter and span:
+
+```go
+// ... configure OpenTelemetry like in example above
+clue.ConfigureOpenTelemetry(ctx, cfg)
+
+// Create a meter and tracer
+meter := otel.Meter("mymeter")
+tracer := otel.Tracer("mytracer")
+
+// Create a counter
+counter, err := meter.Int64Counter("my.counter",
+ metric.WithDescription("The number of times the service has been called"),
+ metric.WithUnit("{call}"))
+if err != nil {
+ log.Fatalf("failed to create counter: %s", err)
+}
+
+handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Create a span
+ ctx, span := tracer.Start(r.Context(), "myhandler")
+ defer span.End()
+
+ // ... do something
+
+ // Add custom attributes to span and counter
+ attr := attribute.Int("myattr", 42)
+ span.SetAttributes(attr)
+ counter.Add(ctx, 1, metric.WithAttributes(attr))
+
+ // ... do something else
+ if _, err := w.Write([]byte("Hello, World!")); err != nil {
+ log.Errorf(ctx, err, "failed to write response")
+ }
+ })
+```
+
+## Goa
+
+The `log` package provides a Goa endpoint middleware that adds the service and
+method names to the logger context. The `debug` package provides a Goa endpoint
+middleware that logs the request and response payloads when debug logging is
+enabled. The example below is a snippet extracted from the `main` function of
+the
+[genforecaster](example/weather/services/forecaster/cmd/forecaster/main.go#L88)
+service:
+
+```go
+svc := forecaster.New(wc)
+endpoints := genforecaster.NewEndpoints(svc)
+endpoints.Use(debug.LogPayloads())
+endpoints.Use(log.Endpoint)
+```
+
+## Example
+
+The [weather](example/weather) example illustrates how to use Clue to instrument
+a system of Goa microservices. The example comes with a set of scripts that can
+be used to install all necessary dependencies including the
+[SigNoz](https://signoz.io/) instrumentation backend. See the
[README](example/weather/README.md) for more information.
+## Migrating from v0.x to v1.x
+
+The v1.0.0 release of `clue` is a major release that introduces breaking
+changes. The following sections describe the changes and how to migrate.
+
+### Initialization
+
+The `clue` package provides a cohesive API for both metrics and tracing,
+effectively replacing the previous `metrics` and `trace` packages. The
+traditional `Context` function, utilized in the `metrics` and `trace` packages
+for setting up telemetry, has been deprecated. In its place, the `clue` package
+introduces the `NewConfig` function, which generates a `Config` object used in
+conjunction with the `ConfigureOpenTelemetry` function to facilitate telemetry
+setup.
+
+v0.x:
+
+```go
+ctx := log.Context(context.Background())
+traceExporter := tracestdout.New()
+metricsExporter := metricstdout.New()
+ctx = trace.Context(ctx, "service", traceExporter)
+ctx = metrics.Context(ctx, "service", metricsExporter)
+```
+
+v1.x:
+
+```go
+ctx := log.Context(context.Background())
+traceExporter := tracestdout.New()
+metricsExporter := metricstdout.New()
+cfg := clue.NewConfig(ctx, "service", "1.0.0", metricsExporter, traceExporter)
+clue.ConfigureOpenTelemetry(ctx, cfg)
+```
+
+### Instrumentation
+
+Instrumenting a HTTP service is now done using the standard `otelhttp` package:
+
+v0.x:
+
+```go
+handler = trace.HTTP(ctx)(handler)
+handler = metrics.HTTP(ctx)(handler)
+http.ListenAndServe(":8080", handler)
+```
+
+v1.x:
+
+```go
+http.ListenAndServe(":8080", otelhttp.NewHandler("service", handler))
+```
+
+Similarly, instrumenting a gRPC service is now done using the standard
+`otelgrpc` package. v1.x also switches to using a gRPC stats handler instead of
+interceptors:
+
+v0.x:
+
+```go
+grpcsvr := grpc.NewServer(
+ grpcmiddleware.WithUnaryServerChain(
+ trace.UnaryServerInterceptor(ctx),
+ metrics.UnaryServerInterceptor(ctx),
+ ),
+ grpcmiddleware.WithStreamServerChain(
+ trace.StreamServerInterceptor(ctx),
+ metrics.StreamServerInterceptor(ctx),
+ ),
+)
+```
+
+v1.x:
+
+```go
+grpcsvr := grpc.NewServer(grpc.StatsHandler(otelgrpc.NewServerHandler()))
+```
+
+### Goa
+
+The `otel` Goa plugin leverages the OpenTelemetry API to annotate spans and
+metrics with HTTP routes.
+
+v0.x:
+
+```go
+mux := goahttp.NewMuxer()
+ctx = metrics.Context(ctx, genfront.ServiceName,
+ metrics.WithRouteResolver(func(r *http.Request) string {
+ return mux.ResolvePattern(r)
+ }),
+)
+```
+
+v1.x:
+
+```go
+package design
+
+import (
+ . "goa.design/goa/v3/dsl"
+ _ "goa.design/plugins/clue"
+)
+```
+
## Contributing
See [Contributing](CONTRIBUTING.md)
diff --git a/clue/config.go b/clue/config.go
new file mode 100644
index 00000000..9065c9c9
--- /dev/null
+++ b/clue/config.go
@@ -0,0 +1,137 @@
+package clue
+
+import (
+ "context"
+
+ "github.com/go-logr/logr"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/metric"
+ metricnoop "go.opentelemetry.io/otel/metric/noop"
+ "go.opentelemetry.io/otel/propagation"
+ sdkmetric "go.opentelemetry.io/otel/sdk/metric"
+ "go.opentelemetry.io/otel/sdk/resource"
+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
+ semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
+ "go.opentelemetry.io/otel/trace"
+ tracenoop "go.opentelemetry.io/otel/trace/noop"
+
+ "goa.design/clue/log"
+)
+
+type (
+ // Config is used to initialize metrics and tracing.
+ Config struct {
+ // MeterProvider is the OpenTelemetry meter provider used by the clue
+ // metrics package.
+ MeterProvider metric.MeterProvider
+ // TracerProvider is the OpenTelemetry tracer provider used by the clue
+ // trace package.
+ TracerProvider trace.TracerProvider
+ // Propagators is the OpenTelemetry propagator used by the clue trace
+ // package.
+ Propagators propagation.TextMapPropagator
+ // ErrorHandler is the error handler used by the OpenTelemetry
+ // package.
+ ErrorHandler otel.ErrorHandler
+ }
+)
+
+// ConfigureOpenTelemetry sets up code instrumentation using the OpenTelemetry
+// API. It leverages the clue logger configured in ctx to log errors.
+func ConfigureOpenTelemetry(ctx context.Context, cfg *Config) {
+ otel.SetMeterProvider(cfg.MeterProvider)
+ otel.SetTracerProvider(cfg.TracerProvider)
+ otel.SetTextMapPropagator(cfg.Propagators)
+ otel.SetLogger(logr.New(log.ToLogrSink(ctx)))
+ otel.SetErrorHandler(cfg.ErrorHandler)
+}
+
+// NewConfig creates a new Config object adequate for use by
+// ConfigureOpenTelemetry. The metricsExporter and spanExporter are used to
+// record telemetry. If either is nil then the corresponding package will not
+// record any telemetry. The OpenTelemetry metrics provider is configured with a
+// periodic reader. The OpenTelemetry tracer provider is configured to use a
+// batch span processor and an adaptive sampler that aims at a maximum sampling
+// rate of requests per second. The resulting configuration can be modified
+// (and providers replaced) by the caller prior to calling
+// ConfigureOpenTelemetry.
+//
+// Example:
+//
+// metricsExporter, err := stdoutmetric.New()
+// if err != nil {
+// return err
+// }
+// spanExporter, err := stdouttrace.New()
+// if err != nil {
+// return err
+// }
+// cfg := clue.NewConfig("mysvc", "1.0.0", metricsExporter, spanExporter)
+func NewConfig(
+ ctx context.Context,
+ svcName string,
+ svcVersion string,
+ metricsExporter sdkmetric.Exporter,
+ spanExporter sdktrace.SpanExporter,
+ opts ...Option,
+) (*Config, error) {
+ options := defaultOptions(ctx)
+ for _, o := range opts {
+ o(options)
+ }
+ res, err := resource.Merge(
+ resource.Default(),
+ resource.NewWithAttributes(
+ semconv.SchemaURL,
+ semconv.ServiceNameKey.String(svcName),
+ semconv.ServiceVersionKey.String(svcVersion),
+ ))
+ if err != nil {
+ return nil, err
+ }
+ var meterProvider metric.MeterProvider
+ if metricsExporter == nil {
+ meterProvider = metricnoop.NewMeterProvider()
+ } else {
+ var reader sdkmetric.Reader
+ if options.readerInterval == 0 {
+ reader = sdkmetric.NewPeriodicReader(metricsExporter)
+ } else {
+ reader = sdkmetric.NewPeriodicReader(
+ metricsExporter,
+ sdkmetric.WithInterval(options.readerInterval),
+ )
+ }
+ meterProvider = sdkmetric.NewMeterProvider(
+ sdkmetric.WithResource(res),
+ sdkmetric.WithReader(reader),
+ )
+ }
+ var tracerProvider trace.TracerProvider
+ if spanExporter == nil {
+ tracerProvider = tracenoop.NewTracerProvider()
+ } else {
+ sampler := sdktrace.ParentBased(
+ AdaptiveSampler(options.maxSamplingRate, options.sampleSize),
+ )
+ tracerProvider = sdktrace.NewTracerProvider(
+ sdktrace.WithResource(res),
+ sdktrace.WithSampler(sampler),
+ sdktrace.WithBatcher(spanExporter),
+ )
+ }
+ return &Config{
+ MeterProvider: meterProvider,
+ TracerProvider: tracerProvider,
+ Propagators: options.propagators,
+ ErrorHandler: options.errorHandler,
+ }, nil
+}
+
+// NewErrorHandler returns an error handler that logs errors using the clue
+// logger configured in ctx.
+func NewErrorHandler(ctx context.Context) otel.ErrorHandler {
+ return otel.ErrorHandlerFunc(func(err error) {
+ log.Error(ctx, err)
+ })
+}
diff --git a/clue/config_test.go b/clue/config_test.go
new file mode 100644
index 00000000..9f54e567
--- /dev/null
+++ b/clue/config_test.go
@@ -0,0 +1,140 @@
+package clue
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
+ "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
+ "go.opentelemetry.io/otel/metric"
+ metricnoop "go.opentelemetry.io/otel/metric/noop"
+ "go.opentelemetry.io/otel/propagation"
+ sdkmetric "go.opentelemetry.io/otel/sdk/metric"
+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
+ "go.opentelemetry.io/otel/trace"
+ tracenoop "go.opentelemetry.io/otel/trace/noop"
+ "goa.design/clue/log"
+)
+
+type (
+ // dummyErrorHandler is a dummy implementation of the OpenTelemetry error handler interface.
+ dummyErrorHandler struct{}
+)
+
+func TestConfigureOpenTelemetry(t *testing.T) {
+ ctx := log.Context(context.Background())
+ noopMeterProvider := metricnoop.NewMeterProvider()
+ noopTracerProvider := tracenoop.NewTracerProvider()
+ noopErrorHandler := dummyErrorHandler{}
+
+ cases := []struct {
+ name string
+ meterProvider metric.MeterProvider
+ tracerProvider trace.TracerProvider
+ propagators propagation.TextMapPropagator
+ errorHandler otel.ErrorHandler
+
+ wantMeterProvider metric.MeterProvider
+ wantTracerProvider trace.TracerProvider
+ wantPropagators propagation.TextMapPropagator
+ wantErrorHandler bool
+ }{
+ {
+ name: "default",
+ }, {
+ name: "meter provider",
+ meterProvider: noopMeterProvider,
+ wantMeterProvider: noopMeterProvider,
+ }, {
+ name: "tracer provider",
+ tracerProvider: noopTracerProvider,
+ wantTracerProvider: noopTracerProvider,
+ }, {
+ name: "propagators",
+ propagators: propagation.Baggage{},
+ wantPropagators: propagation.Baggage{},
+ }, {
+ name: "error handler",
+ errorHandler: &noopErrorHandler,
+ wantErrorHandler: true,
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ cfg := &Config{
+ MeterProvider: c.meterProvider,
+ TracerProvider: c.tracerProvider,
+ Propagators: c.propagators,
+ ErrorHandler: c.errorHandler,
+ }
+ ConfigureOpenTelemetry(ctx, cfg)
+ assert.Equal(t, c.wantMeterProvider, otel.GetMeterProvider())
+ assert.Equal(t, c.wantTracerProvider, otel.GetTracerProvider())
+ assert.Equal(t, c.wantPropagators, otel.GetTextMapPropagator())
+ })
+ }
+}
+
+func TestNewConfig(t *testing.T) {
+ ctx := log.Context(context.Background())
+ svcName := "svcName"
+ svcVersion := "svcVersion"
+ spanExporter, err := stdouttrace.New()
+ require.NoError(t, err)
+ metricsExporter, err := stdoutmetric.New()
+ require.NoError(t, err)
+ noopErrorHandler := dummyErrorHandler{}
+
+ cases := []struct {
+ name string
+ metricsExporter sdkmetric.Exporter
+ spanExporter sdktrace.SpanExporter
+ propagators propagation.TextMapPropagator
+ errorHandler otel.ErrorHandler
+
+ wantPropagators propagation.TextMapPropagator
+ wantErrorHandler bool
+ }{
+ {
+ name: "default",
+ }, {
+ name: "metrics exporter",
+ metricsExporter: metricsExporter,
+ }, {
+ name: "tracer provider",
+ spanExporter: spanExporter,
+ }, {
+ name: "propagators",
+ propagators: propagation.Baggage{},
+ wantPropagators: propagation.Baggage{},
+ }, {
+ name: "error handler",
+ errorHandler: &noopErrorHandler,
+ wantErrorHandler: true,
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ cfg, err := NewConfig(ctx,
+ svcName,
+ svcVersion,
+ c.metricsExporter,
+ c.spanExporter,
+ WithPropagators(c.propagators),
+ WithErrorHandler(c.errorHandler))
+ assert.NoError(t, err)
+ if c.spanExporter != nil {
+ serialized := fmt.Sprintf("%+v", cfg.TracerProvider)
+ assert.Contains(t, serialized, "maxSamplingRate:2")
+ }
+ assert.Equal(t, c.wantPropagators, cfg.Propagators)
+ assert.Equal(t, c.wantErrorHandler, cfg.ErrorHandler != nil)
+ })
+ }
+}
+
+func (dummyErrorHandler) Handle(error) {}
diff --git a/clue/options.go b/clue/options.go
new file mode 100644
index 00000000..a7d25f6c
--- /dev/null
+++ b/clue/options.go
@@ -0,0 +1,79 @@
+package clue
+
+import (
+ "context"
+ "time"
+
+ "go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/propagation"
+)
+
+type (
+ // Option is a function that initializes the clue configuration.
+ Option func(*options)
+
+ // options contains the clue configuration options.
+ options struct {
+ // readerInterval is the interval at which the metrics reader is
+ // invoked.
+ readerInterval time.Duration
+ // maxSamplingRate is the maximum sampling rate for the trace exporter.
+ maxSamplingRate int
+ // sampleSize is the number of requests between two adjustments of the
+ // sampling rate.
+ sampleSize int
+ // propagators is the trace propagators.
+ propagators propagation.TextMapPropagator
+ // errorHandler is the error handler used by the otel package.
+ errorHandler otel.ErrorHandler
+ }
+)
+
+// defaultOptions returns a new options struct with default values.
+// The logger in ctx is used to log errors.
+func defaultOptions(ctx context.Context) *options {
+ return &options{
+ maxSamplingRate: 2,
+ sampleSize: 10,
+ propagators: propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}),
+ errorHandler: NewErrorHandler(ctx),
+ }
+}
+
+// WithReaderInterval returns an option that sets the interval at which the
+// metrics reader is invoked.
+func WithReaderInterval(interval time.Duration) Option {
+ return func(c *options) {
+ c.readerInterval = interval
+ }
+}
+
+// WithMaxSamplingRate sets the maximum sampling rate in requests per second.
+func WithMaxSamplingRate(rate int) Option {
+ return func(opts *options) {
+ opts.maxSamplingRate = rate
+ }
+}
+
+// WithSampleSize sets the number of requests between two adjustments of the
+// sampling rate.
+func WithSampleSize(size int) Option {
+ return func(opts *options) {
+ opts.sampleSize = size
+ }
+}
+
+// WithPropagators sets the propagators when extracting and injecting trace
+// context.
+func WithPropagators(propagator propagation.TextMapPropagator) Option {
+ return func(opts *options) {
+ opts.propagators = propagator
+ }
+}
+
+// WithErrorHandler sets the error handler used by the telemetry package.
+func WithErrorHandler(errorHandler otel.ErrorHandler) Option {
+ return func(opts *options) {
+ opts.errorHandler = errorHandler
+ }
+}
diff --git a/clue/options_test.go b/clue/options_test.go
new file mode 100644
index 00000000..44f9f3b6
--- /dev/null
+++ b/clue/options_test.go
@@ -0,0 +1,65 @@
+package clue
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "go.opentelemetry.io/otel/propagation"
+ "goa.design/clue/log"
+)
+
+func TestOptions(t *testing.T) {
+ ctx := log.Context(context.Background())
+ cases := []struct {
+ name string
+ option Option
+ want func(*options) // mutate default options
+ }{
+ {
+ name: "default",
+ },
+ {
+ name: "with reader interval",
+ option: WithReaderInterval(1000),
+ want: func(o *options) { o.readerInterval = 1000 },
+ },
+ {
+ name: "with max sampling rate",
+ option: WithMaxSamplingRate(1000),
+ want: func(o *options) { o.maxSamplingRate = 1000 },
+ },
+ {
+ name: "with sample size",
+ option: WithSampleSize(1000),
+ want: func(o *options) { o.sampleSize = 1000 },
+ },
+ {
+ name: "with propagator",
+ option: WithPropagators(propagation.TraceContext{}),
+ want: func(o *options) { o.propagators = propagation.TraceContext{} },
+ },
+ {
+ name: "with error handler",
+ option: WithErrorHandler(dummyErrorHandler{}),
+ want: func(o *options) { o.errorHandler = dummyErrorHandler{} },
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ got := defaultOptions(ctx)
+ if c.option != nil {
+ c.option(got)
+ }
+ want := defaultOptions(ctx)
+ if c.want != nil {
+ c.want(want)
+ }
+ assert.Equal(t, want.maxSamplingRate, got.maxSamplingRate)
+ assert.Equal(t, want.sampleSize, got.sampleSize)
+ assert.Equal(t, want.readerInterval, got.readerInterval)
+ assert.Equal(t, want.propagators, got.propagators)
+ assert.IsType(t, want.errorHandler, got.errorHandler)
+ })
+ }
+}
diff --git a/trace/sampler.go b/clue/sampler.go
similarity index 73%
rename from trace/sampler.go
rename to clue/sampler.go
index cc2c059f..9f9418f6 100644
--- a/trace/sampler.go
+++ b/clue/sampler.go
@@ -1,10 +1,9 @@
-package trace
+package clue
import (
"fmt"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
- "go.opentelemetry.io/otel/trace"
"goa.design/goa/v3/middleware"
)
@@ -15,15 +14,15 @@ type sampler struct {
sampleSize int
}
-// adaptiveSampler computes the interval for sampling for tracing middleware.
-// it can also be used by non-web go routines to trace internal API calls.
+// AdaptiveSampler returns a trace sampler that dynamically computes the
+// interval between samples to target a desired maximum sampling rate.
//
// maxSamplingRate is the desired maximum sampling rate in requests per second.
//
// sampleSize sets the number of requests between two adjustments of the
// sampling rate when MaxSamplingRate is set. the sample rate cannot be adjusted
// until the sample size is reached at least once.
-func adaptiveSampler(maxSamplingRate, sampleSize int) sdktrace.Sampler {
+func AdaptiveSampler(maxSamplingRate, sampleSize int) sdktrace.Sampler {
return sampler{
s: middleware.NewAdaptiveSampler(maxSamplingRate, sampleSize),
maxSamplingRate: maxSamplingRate,
@@ -41,10 +40,5 @@ func (s sampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingRe
if !s.s.Sample() {
return sdktrace.SamplingResult{Decision: sdktrace.Drop}
}
-
- psc := trace.SpanContextFromContext(p.ParentContext)
- return sdktrace.SamplingResult{
- Decision: sdktrace.RecordAndSample,
- Tracestate: psc.TraceState(),
- }
+ return sdktrace.SamplingResult{Decision: sdktrace.RecordAndSample}
}
diff --git a/clue/sampler_test.go b/clue/sampler_test.go
new file mode 100644
index 00000000..a3f1008c
--- /dev/null
+++ b/clue/sampler_test.go
@@ -0,0 +1,24 @@
+package clue
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
+)
+
+func TestAdaptiveSampler(t *testing.T) {
+ // We don't need to test Goa, keep it simple...
+ s := AdaptiveSampler(2, 10)
+ expected := "Adaptive{maxSamplingRate:2,sampleSize:10}"
+ assert.Equal(t, expected, s.Description())
+ res := s.ShouldSample(sdktrace.SamplingParameters{})
+ assert.Equal(t, sdktrace.SamplingResult{Decision: sdktrace.RecordAndSample}, res)
+ s2 := AdaptiveSampler(1, 2)
+ expected = "Adaptive{maxSamplingRate:1,sampleSize:2}"
+ assert.Equal(t, expected, s2.Description())
+ res2 := s2.ShouldSample(sdktrace.SamplingParameters{})
+ res3 := s2.ShouldSample(sdktrace.SamplingParameters{})
+ assert.Equal(t, sdktrace.SamplingResult{Decision: sdktrace.RecordAndSample}, res2)
+ assert.Equal(t, sdktrace.SamplingResult{Decision: sdktrace.Drop}, res3)
+}
diff --git a/debug/debug.go b/debug/debug.go
index 4915c183..a768c961 100644
--- a/debug/debug.go
+++ b/debug/debug.go
@@ -24,18 +24,19 @@ var (
debugLogs bool
)
-// MountDebugLogEnabler mounts an endpoint under "/debug" and returns a HTTP
-// middleware that manages the status of debug logs. The endpoint accepts a
-// single query parameter "debug-logs". If the parameter is set to "on" then
-// debug logs are enabled. If the parameter is set to "off" then debug logs are
-// disabled. In all other cases the endpoint returns the current debug logs
-// status. The path, query parameter name and values can be changed using the
-// WithPath, WithQuery, WithOnValue and WithOffValue options.
+// MountDebugLogEnabler mounts an endpoint under "/debug" that manages the
+// status of debug logs. The endpoint accepts a single query parameter
+// "debug-logs". If the parameter is set to "on" then debug logs are enabled. If
+// the parameter is set to "off" then debug logs are disabled. In all other
+// cases the endpoint returns the current debug logs status. The path, query
+// parameter name and values can be changed using the WithPath, WithQuery,
+// WithOnValue and WithOffValue options.
//
// Note: the endpoint merely controls the status of debug logs. It does not
// actually configure the current logger. The logger is configured by the
// middleware returned by the HTTP function or by the gRPC interceptors returned
-// by the UnaryServerInterceptor and StreamServerInterceptor functions.
+// by the UnaryServerInterceptor and StreamServerInterceptor functions which
+// should be used in conjunction with the MountDebugLogEnabler function.
func MountDebugLogEnabler(mux Muxer, opts ...DebugLogEnablerOption) {
o := defaultDebugLogEnablerOptions()
for _, opt := range opts {
diff --git a/debug/grpc_test.go b/debug/grpc_test.go
index b902983b..4cba704e 100644
--- a/debug/grpc_test.go
+++ b/debug/grpc_test.go
@@ -7,6 +7,7 @@ import (
"net/http/httptest"
"testing"
+ "github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"goa.design/clue/internal/testsvc"
@@ -18,7 +19,7 @@ func TestUnaryServerInterceptor(t *testing.T) {
ctx := log.Context(context.Background(), log.WithOutput(&buf), log.WithFormat(logKeyValsOnly))
cli, stop := testsvc.SetupGRPC(t,
testsvc.WithServerOptions(grpc.ChainUnaryInterceptor(
- log.UnaryServerInterceptor(ctx, log.WithDisableCallLogging()),
+ log.UnaryServerInterceptor(ctx, log.WithDisableCallLogging(), log.WithDisableCallID()),
UnaryServerInterceptor())),
testsvc.WithUnaryFunc(logUnaryMethod))
defer stop()
@@ -28,10 +29,10 @@ func TestUnaryServerInterceptor(t *testing.T) {
defer ts.Close()
steps := []struct {
- name string
- on bool
- off bool
- expectedLogs string
+ name string
+ on bool
+ off bool
+ wantLog string
}{
{"start", false, false, ""},
{"turn debug logs on", true, false, "debug=message "},
@@ -47,12 +48,8 @@ func TestUnaryServerInterceptor(t *testing.T) {
makeRequest(t, ts.URL+"/debug?debug-logs=off")
}
_, err := cli.GRPCMethod(context.Background(), nil)
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- if buf.String() != step.expectedLogs {
- t.Errorf("expected log %q, got %q", step.expectedLogs, buf.String())
- }
+ assert.NoError(t, err)
+ assert.Equal(t, step.wantLog, buf.String())
buf.Reset()
}
}
@@ -62,14 +59,14 @@ func TestStreamServerInterceptor(t *testing.T) {
ctx := log.Context(context.Background(), log.WithOutput(&buf), log.WithFormat(logKeyValsOnly))
cli, stop := testsvc.SetupGRPC(t,
testsvc.WithServerOptions(grpc.ChainStreamInterceptor(
- log.StreamServerInterceptor(ctx, log.WithDisableCallLogging()),
+ log.StreamServerInterceptor(ctx, log.WithDisableCallLogging(), log.WithDisableCallID()),
StreamServerInterceptor())),
testsvc.WithStreamFunc(echoMethod))
defer stop()
steps := []struct {
name string
enableDebugLogs bool
- expectedLogs string
+ wantLog string
}{
{"no debug logs", false, ""},
{"debug logs", true, "debug=message "},
@@ -78,19 +75,13 @@ func TestStreamServerInterceptor(t *testing.T) {
for _, step := range steps {
debugLogs = step.enableDebugLogs
stream, err := cli.GRPCStream(context.Background())
- if err != nil {
- t.Errorf("%s: unexpected error: %v", step.name, err)
- }
+ assert.NoError(t, err)
defer stream.Close()
- if err = stream.Send(&testsvc.Fields{}); err != nil {
- t.Errorf("%s: unexpected send error: %v", step.name, err)
- }
- if _, err = stream.Recv(); err != nil {
- t.Errorf("%s: unexpected recv error: %v", step.name, err)
- }
- if buf.String() != step.expectedLogs {
- t.Errorf("%s: unexpected log %q", step.name, buf.String())
- }
+ err = stream.Send(&testsvc.Fields{})
+ assert.NoError(t, err)
+ _, err = stream.Recv()
+ assert.NoError(t, err)
+ assert.Equal(t, step.wantLog, buf.String())
buf.Reset()
}
}
diff --git a/debug/http_test.go b/debug/http_test.go
index 3148e1e3..2d1d7d1d 100644
--- a/debug/http_test.go
+++ b/debug/http_test.go
@@ -7,13 +7,17 @@ import (
"net/http/httptest"
"testing"
+ "github.com/stretchr/testify/assert"
+
"goa.design/clue/log"
)
func TestHTTP(t *testing.T) {
// Create log context
var buf bytes.Buffer
- ctx := log.Context(context.Background(), log.WithOutput(&buf), log.WithFormat(logKeyValsOnly))
+ ctx := log.Context(context.Background(),
+ log.WithOutput(&buf),
+ log.WithFormat(logKeyValsOnly))
log.FlushAndDisableBuffering(ctx)
// Create HTTP handler
@@ -32,7 +36,9 @@ func TestHTTP(t *testing.T) {
// Mount debug handler and log middleware
MountDebugLogEnabler(mux)
handler = HTTP()(handler)
- handler = log.HTTP(ctx, log.WithDisableRequestLogging())(handler)
+ handler = log.HTTP(ctx,
+ log.WithDisableRequestLogging(),
+ log.WithDisableRequestID())(handler)
// Start test server
mux.Handle("/", handler)
@@ -40,17 +46,16 @@ func TestHTTP(t *testing.T) {
defer ts.Close()
steps := []struct {
- name string
- on bool
- off bool
- expectedResp string
- expectedLogs string
+ name string
+ on bool
+ off bool
+ wantLog string
}{
- {"start", false, false, "", "test=info "},
- {"turn debug logs on", true, false, `{"debug-logs":true}`, "test=info test=debug "},
- {"with debug logs on", false, false, `{"debug-logs":true}`, "test=info test=debug "},
- {"turn debug logs off", false, true, `{"debug-logs":false}`, "test=info "},
- {"with debug logs off", false, false, `{"debug-logs":false}`, "test=info "},
+ {"start", false, false, "test=info "},
+ {"turn debug logs on", true, false, "test=info test=debug "},
+ {"with debug logs on", false, false, "test=info test=debug "},
+ {"turn debug logs off", false, true, "test=info "},
+ {"with debug logs off", false, false, "test=info "},
}
for _, step := range steps {
if step.on {
@@ -62,15 +67,9 @@ func TestHTTP(t *testing.T) {
status, resp := makeRequest(t, ts.URL)
- if status != http.StatusOK {
- t.Errorf("%s: got status %d, expected %d", step.name, status, http.StatusOK)
- }
- if resp != "OK" {
- t.Errorf("%s: got body %q, expected %q", step.name, resp, "OK")
- }
- if buf.String() != step.expectedLogs {
- t.Errorf("%s: got logs %q, expected %q", step.name, buf.String(), step.expectedLogs)
- }
+ assert.Equal(t, http.StatusOK, status)
+ assert.Equal(t, "OK", resp)
+ assert.Equal(t, step.wantLog, buf.String())
buf.Reset()
}
}
diff --git a/example/weather/README.md b/example/weather/README.md
index e26da310..9f3fe1e3 100644
--- a/example/weather/README.md
+++ b/example/weather/README.md
@@ -22,49 +22,49 @@ scripts/setup
scripts/server
```
-`scripts/setup` download build dependencies and compiles the services.
+`scripts/setup` configures all the required dependencies and compiles the services.
`scripts/server` runs the services using
[overmind](https://github.com/DarthSim/overmind). `scripts/server` also starts
-`docker-compose` with a configuration that runs the Grafana agent, cortex, tempo
-and dashboard locally.
+`docker-compose` with a configuration that runs a self-hosted deployment of
+[SigNoz](https://signoz.io/) as backend for instrumentation.
### Making a Request
Assuming you have a running weather system, you can make a request to the front
-service using the `curl` command:
+service using the `curl` command as follows:
```bash
curl http://localhost:8084/forecast/8.8.8.8
```
-### Looking at Traces
+If this returns successfully start the script that generates load:
-To analyze traces:
-
-* Retrieve the front service trace ID from its logs, for example:
-
-```text
-front | DEBG[0003] svc=front request-id=aZtVOM7L trace-id=fcb9bb474db0b095923b110b7c1cdcab
+```bash
+scripts/load
```
-* Open the Grafana dashboard running on
- [http://localhost:3000](http://localhost:3000), click on `Explore` in the left
- pane and select `Tempo` in the top dropdown. Enter the trace ID and voila:
+### Looking at Telemetry Data
-![Tempo Screenshot](./images/tempo.png)
+To analyze traces open the SigNoz dashboard running on
+[http://localhost:3301](http://localhost:3301).
+
+![SigNoz Screenshot](./images/signoz.png)
## Instrumentation
### Logging
The three services make use of the
-[log](https://github.com/goadesign/clue/tree/main/log) package. The package
-is initialized with the key / value pair `svc`:``, for example:
+[log](https://github.com/goadesign/clue/tree/main/log) package initialized in
+`main`:
```go
-ctx := log.With(log.Context(context.Background()), "svc", genfront.ServiceName)
+ctx := log.Context(context.Background(), log.WithFormat(format), log.WithFunc(log.Span))
```
+The `log.WithFormat` option enables colors when logging to a terminal while the
+`log.Span` option adds the trace and span IDs to each log entry.
+
The `front` service uses the HTTP middleware to initialize the log context for
for every request:
@@ -83,107 +83,83 @@ The gRPC services (`locator` and `forecaster`) use the gRPC interceptor returned
```go
grpcsvr := grpc.NewServer(
- grpcmiddleware.WithUnaryServerChain(
- goagrpcmiddleware.UnaryRequestID(),
- log.UnaryServerInterceptor(ctx), // <--
- goagrpcmiddleware.UnaryServerLogContext(log.AsGoaMiddlewareLogger),
- metrics.UnaryServerInterceptor(ctx, genforecast.ServiceName),
- trace.UnaryServerInterceptor(ctx),
- ))
-```
-
-### Tracing
-
-The example runs a [Grafana agent](https://grafana.com/docs/grafana-cloud/agent/)
-configured to listen to OLTP gRPC requests. The agent forwards the traces to
-the [Tempo](https://grafana.com/docs/tempo/latest/) service also running locally.
-
-Each service uses the
-[trace](https://github.com/goadesign/clue/tree/main/trace) package to ship
-traces to the agent:
-
-```go
-conn, err := grpc.DialContext(ctx, *collectorAddr,
- grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithBlock())
-ctx, err = trace.Context(ctx, genfront.ServiceName, trace.WithGRPCExporter(conn))
-```
-
-gRPC services use the `trace.UnaryServerInterceptor` to create a span for each
-request:
-
-```go
-grpcsvr := grpc.NewServer(
- grpcmiddleware.WithUnaryServerChain(
- goagrpcmiddleware.UnaryRequestID(),
- log.UnaryServerInterceptor(ctx),
- goagrpcmiddleware.UnaryServerLogContext(log.AsGoaMiddlewareLogger),
- metrics.UnaryServerInterceptor(ctx, genforecast.ServiceName),
- trace.UnaryServerInterceptor(ctx), // <--
- ))
-```
-
-The front service uses the `trace.HTTP` middleware to create a span for each
-request:
-
-```go
-handler = trace.HTTP(ctx)(handler)
-```
-
-HTTP dependency clients use the `trace.Client` middleware to create spans for
-each outgoing request:
-
-```go
-c := &http.Client{Transport: trace.Client(ctx, http.DefaultTransport)}
+ grpc.ChainUnaryInterceptor(
+ log.UnaryServerInterceptor(ctx), // <--
+ debug.UnaryServerInterceptor()),
+ grpc.StatsHandler(otelgrpc.NewServerHandler()))
```
-gRPC dependency clients use the `trace.UnaryClientInterceptor` interceptor to
-create spans for each outgoing request:
+### Metrics and Tracing
-```go
-lcc, err := grpc.DialContext(ctx, *locatorAddr,
- grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithUnaryInterceptor(trace.UnaryClientInterceptor(ctx)))
-```
-
-### Metrics
+The example runs a self-hosted deployment of [SigNoz](https://signoz.io/) as backend for instrumentation. Each service sends telemetry data to the OpenTelemetry collector running in the `docker-compose` configuration. The collector then forwards the data to the SigNoz backend.
-The `metrics` package provides a set of instrumentation middleware that
-collects metrics from HTTP and gRPC servers and sends them to the
-[Tempo](https://grafana.com/docs/tempo/latest/) service.
-
-First the context is initialized with the service name and optional
-options:
+The collector is configured in the `main` function of each service:
```go
-ctx = metrics.Context(ctx, genfront.ServiceName)
-```
-
-The gRPC services are instrumented with the `metrics.UnaryServerInterceptor`
-interceptor:
-
-```go
-grpcsvr := grpc.NewServer(
- grpcmiddleware.WithUnaryServerChain(
- goagrpcmiddleware.UnaryRequestID(),
- log.UnaryServerInterceptor(ctx),
- goagrpcmiddleware.UnaryServerLogContext(log.AsGoaMiddlewareLogger),
- metrics.UnaryServerInterceptor(ctx), // <--
- trace.UnaryServerInterceptor(ctx),
- ))
+spanExporter, err := otlptracegrpc.New(ctx,
+ otlptracegrpc.WithEndpoint(*oteladdr),
+ otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()))
+if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize tracing")
+}
+defer func() {
+ if err := spanExporter.Shutdown(ctx); err != nil {
+ log.Errorf(ctx, err, "failed to shutdown tracing")
+ }
+}()
+metricExporter, err := otlpmetricgrpc.New(ctx,
+ otlpmetricgrpc.WithEndpoint(*oteladdr),
+ otlpmetricgrpc.WithTLSCredentials(insecure.NewCredentials()))
+if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize metrics")
+}
+defer func() {
+ if err := metricExporter.Shutdown(ctx); err != nil {
+ log.Errorf(ctx, err, "failed to shutdown metrics")
+ }
+}()
+cfg, err := clue.NewConfig(ctx,
+ genforecaster.ServiceName,
+ genforecaster.APIVersion,
+ metricExporter,
+ spanExporter,
+)
+if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize instrumentation")
+}
+clue.ConfigureOpenTelemetry(ctx, cfg)
```
-The front service is instrumented with the `metrics.HTTP` middleware:
+Clients of downstream services are also instrumented using OpenTelemetry. For
+example the following code creates an instrumented gRPC connection and uses it
+to create a client to the `Locator` service:
```go
-handler = metrics.HTTP(ctx)(handler)
+lcc, err := grpc.DialContext(ctx,
+ *locatorAddr,
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()), // Log requests
+ grpc.WithStatsHandler(otelgrpc.NewClientHandler())) // Collect metrics and traces
+if err != nil {
+ log.Fatalf(ctx, err, "failed to connect to locator")
+}
+lc := locator.New(lcc) // Create client using instrumented connection
```
-All the services run a HTTP server that exposes a Prometheus metrics endpoint at
-`/metrics`.
+The example also showcases the instrumentation of clients to external services.
+For example, the `Locator` service demonstrates how the HTTP client used to
+create the IP Location service client is instrumented:
```go
-http.Handle("/metrics", metrics.Handler(ctx))
+httpc := &http.Client{
+ Transport: log.Client( // Log requests
+ otelhttp.NewTransport(
+ http.DefaultTransport,
+ otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
+ return otelhttptrace.NewClientTrace(ctx)
+ }), // Propagate traces
+ ))}
+ipc := ipapi.New(httpc)
```
### Health Checks
diff --git a/example/weather/docker-compose/agent/config/agent.yaml b/example/weather/docker-compose/agent/config/agent.yaml
deleted file mode 100644
index 3b90a82c..00000000
--- a/example/weather/docker-compose/agent/config/agent.yaml
+++ /dev/null
@@ -1,76 +0,0 @@
-server:
- log_level: info
- http_listen_port: 12345
- graceful_shutdown_timeout: 10s
-
-metrics:
- global:
- scrape_interval: 10s
- configs:
- - name: test
- host_filter: false
- scrape_configs:
- - job_name: front_scrape
- static_configs:
- - targets: ['host.docker.internal:8085']
- - job_name: locator_scrape
- static_configs:
- - targets: ['host.docker.internal:8083']
- - job_name: forecast_scrape
- static_configs:
- - targets: ['host.docker.internal:8081']
- - job_name: local_scrape
- static_configs:
- - targets: ['127.0.0.1:12345', '127.0.0.1:8889']
- labels:
- cluster: 'docker_compose'
- container: 'agent'
- pod: 'grafana-agent-local'
- - job_name: cortex_scrape
- static_configs:
- - targets: ['cortex:9009']
- labels:
- cluster: 'docker_compose'
- container: 'cortex'
- remote_write:
- - url: http://cortex:9009/api/prom/push
-
-logs:
- configs:
- - name: default
- positions:
- filename: /tmp/positions.yaml
- clients:
- - url: http://loki:3100/loki/api/v1/push
- scrape_configs:
- - job_name: system
- static_configs:
- - targets:
- - localhost
- labels:
- job: varlogs
- __path__: /var/log/*log
-
-traces:
- configs:
- - name: default
- receivers:
- otlp:
- protocols:
- grpc:
- remote_write:
- - endpoint: tempo:4317
- insecure: true
- batch:
- timeout: 5s
- send_batch_size: 100
- automatic_logging:
- backend: logs_instance
- logs_instance_name: default
- spans: true
- processes: true
- roots: true
- spanmetrics:
- handler_endpoint: 0.0.0.0:8889
- service_graphs:
- enabled: true
diff --git a/example/weather/docker-compose/cortex/config/cortex.yaml b/example/weather/docker-compose/cortex/config/cortex.yaml
deleted file mode 100644
index dfe8243d..00000000
--- a/example/weather/docker-compose/cortex/config/cortex.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-auth_enabled: false
-
-server:
- http_listen_port: 9009
-
- # Configure the server to allow messages up to 100MB.
- grpc_server_max_recv_msg_size: 104857600
- grpc_server_max_send_msg_size: 104857600
- grpc_server_max_concurrent_streams: 1000
-
-distributor:
- shard_by_all_labels: true
- pool:
- health_check_ingesters: true
-
-ingester_client:
- grpc_client_config:
- max_recv_msg_size: 104857600
- max_send_msg_size: 104857600
- grpc_compression: gzip
-
-ingester:
- lifecycler:
- join_after: 0
- min_ready_duration: 0s
- final_sleep: 0s
- num_tokens: 512
-
- ring:
- kvstore:
- store: inmemory
- replication_factor: 1
-
-storage:
- engine: blocks
-
-blocks_storage:
- tsdb:
- dir: /tmp/cortex/tsdb
- bucket_store:
- sync_dir: /tmp/cortex/tsdb-sync
-
- backend: filesystem
- filesystem:
- dir: /tmp/cortex/blocks
-
-compactor:
- data_dir: /tmp/cortex/compactor
- sharding_ring:
- kvstore:
- store: inmemory
-
-frontend_worker:
- match_max_concurrent: true
-
-ruler:
- enable_api: true
- enable_sharding: false
- storage:
- type: local
- local:
- directory: /tmp/cortex/rules
-
-limits:
- ingestion_rate: 250000
- ingestion_burst_size: 500000
diff --git a/example/weather/docker-compose/docker-compose-grafana.yaml b/example/weather/docker-compose/docker-compose-grafana.yaml
deleted file mode 100644
index b216fcfe..00000000
--- a/example/weather/docker-compose/docker-compose-grafana.yaml
+++ /dev/null
@@ -1,59 +0,0 @@
-version: "2"
-services:
- agent:
- image: grafana/agent:latest
- volumes:
- - ./agent/config:/etc/agent-config
- - /var/log:/var/log
- entrypoint:
- - /bin/grafana-agent
- - -config.file=/etc/agent-config/agent.yaml
- - -prometheus.wal-directory=/tmp/agent/wal
- ports:
- - "12345:12345"
- depends_on:
- - tempo
- extra_hosts:
- - "host.docker.internal:${HOST_GATEWAY}"
-
- loki:
- image: grafana/loki:2.2.1
- ports:
- - "3100:3100"
- command: -config.file=/etc/loki/local-config.yaml
-
- cortex:
- image: cortexproject/cortex:v1.8.1
- volumes:
- - /tmp/cortex:/tmp/cortex
- - ./cortex/config:/etc/cortex-config
- entrypoint:
- - /bin/cortex
- - -config.file=/etc/cortex-config/cortex.yaml
- ports:
- - "9009:9009"
-
- # tracing backend
- tempo:
- image: grafana/tempo:latest
- volumes:
- - /tmp/tempo:/tmp/tempo
- - ./tempo/config:/etc/tempo-config
- command:
- - "-config.file=/etc/tempo-config/tempo.yaml"
- ports:
- - "4317:4317"
-
- grafana:
- image: grafana/grafana:7.5.4
- entrypoint:
- - /usr/share/grafana/bin/grafana-server
- - --homepath=/usr/share/grafana
- - --config=/etc/grafana-config/grafana.ini
- volumes:
- - ./grafana/config:/etc/grafana-config
- - ./grafana/datasources:/etc/grafana/provisioning/datasources
- - ./grafana/dashboards-provisioning:/etc/grafana/provisioning/dashboards
- - ./grafana/dashboards:/var/lib/grafana/dashboards
- ports:
- - "3000:3000"
\ No newline at end of file
diff --git a/example/weather/docker-compose/grafana/config/grafana.ini b/example/weather/docker-compose/grafana/config/grafana.ini
deleted file mode 100644
index ba1078c5..00000000
--- a/example/weather/docker-compose/grafana/config/grafana.ini
+++ /dev/null
@@ -1,9 +0,0 @@
-[analytics]
-reporting_enabled = false
-[auth.anonymous]
-enabled = true
-org_role = Admin
-[explore]
-enabled = true
-[users]
-default_theme = dark
diff --git a/example/weather/docker-compose/grafana/dashboards-provisioning/dashboards.yaml b/example/weather/docker-compose/grafana/dashboards-provisioning/dashboards.yaml
deleted file mode 100644
index c038adf8..00000000
--- a/example/weather/docker-compose/grafana/dashboards-provisioning/dashboards.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-apiVersion: 1
-
-providers:
-- name: 'dashboards'
- orgId: 1
- folder: ''
- folderUid: ''
- type: file
- disableDeletion: true
- editable: true
- updateIntervalSeconds: 10
- allowUiUpdates: false
- options:
- path: /var/lib/grafana/dashboards
diff --git a/example/weather/docker-compose/grafana/dashboards/agent-operational.json b/example/weather/docker-compose/grafana/dashboards/agent-operational.json
deleted file mode 100644
index 6d6c1639..00000000
--- a/example/weather/docker-compose/grafana/dashboards/agent-operational.json
+++ /dev/null
@@ -1,1189 +0,0 @@
-{
- "annotations": {
- "list": [ ]
- },
- "editable": true,
- "gnetId": null,
- "graphTooltip": 0,
- "hideControls": false,
- "links": [ ],
- "refresh": "30s",
- "rows": [
- {
- "collapse": false,
- "height": "250px",
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 1,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(go_gc_duration_seconds_count{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"}[5m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "GCs",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 2,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "go_memstats_heap_inuse_bytes{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Go Heap",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "decbytes",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 3,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "go_goroutines{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Goroutines",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 4,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(container_cpu_usage_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"}[5m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "CPU",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 5,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "container_memory_working_set_bytes{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "WSS",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 6,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(promtail_custom_bad_words_total{cluster=~\"$cluster\", exported_namespace=~\"$namespace\", exported_job=~\"$job\"}[5m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{job}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Bad Words",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "General",
- "titleSize": "h6"
- },
- {
- "collapse": false,
- "height": "250px",
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 7,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum by (pod) (rate(container_network_receive_bytes_total{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\"}[5m]))",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "RX by Pod",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 8,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum by (pod) (rate(container_network_transmit_bytes_total{cluster=~\"$cluster\", namespace=~\"$namespace\", pod=~\"$pod\"}[5m]))",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "TX by Pod",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Network",
- "titleSize": "h6"
- },
- {
- "collapse": false,
- "height": "250px",
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 9,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "(sum by (pod) (avg_over_time(go_memstats_heap_inuse_bytes{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"}[1m])))\n/\n(sum by (pod) (agent_wal_storage_active_series{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"}))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Bytes/Series/Pod",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "decbytes",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 10,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "(sum by (container) (avg_over_time(go_memstats_heap_inuse_bytes{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"}[1m])))\n/\n(sum by (container) (agent_wal_storage_active_series{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"}))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{container}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Bytes/Series",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "decbytes",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 11,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum by (pod) (agent_wal_storage_active_series{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"})",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Series/Pod",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 12,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum by (instance_group_name) (agent_wal_storage_active_series{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"})",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{instance_group_name}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Series/Config",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 13,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 2,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum by (container) (agent_wal_storage_active_series{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\", pod=~\"$pod\"})",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{container}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Series",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Prometheus Read",
- "titleSize": "h6"
- }
- ],
- "schemaVersion": 14,
- "style": "dark",
- "tags": [
- "grafana-agent-mixin"
- ],
- "templating": {
- "list": [
- {
- "current": {
- "text": "default",
- "value": "default"
- },
- "hide": 0,
- "label": "Data Source",
- "name": "datasource",
- "options": [ ],
- "query": "prometheus",
- "refresh": 1,
- "regex": "",
- "type": "datasource"
- },
- {
- "allValue": ".+",
- "current": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": "cluster",
- "multi": true,
- "name": "cluster",
- "options": [ ],
- "query": "label_values(agent_build_info, cluster)",
- "refresh": 1,
- "regex": "",
- "sort": 2,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": ".+",
- "current": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": "namespace",
- "multi": true,
- "name": "namespace",
- "options": [ ],
- "query": "label_values(agent_build_info, namespace)",
- "refresh": 1,
- "regex": "",
- "sort": 2,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": ".+",
- "current": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": "container",
- "multi": true,
- "name": "container",
- "options": [ ],
- "query": "label_values(agent_build_info, container)",
- "refresh": 1,
- "regex": "",
- "sort": 2,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": "grafana-agent-.*",
- "current": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": "pod",
- "multi": true,
- "name": "pod",
- "options": [ ],
- "query": "label_values(agent_build_info{container=~\"$container\"}, pod)",
- "refresh": 1,
- "regex": "",
- "sort": 2,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- }
- ]
- },
- "time": {
- "from": "now-1h",
- "to": "now"
- },
- "timepicker": {
- "refresh_intervals": [
- "5s",
- "10s",
- "30s",
- "1m",
- "5m",
- "15m",
- "30m",
- "1h",
- "2h",
- "1d"
- ],
- "time_options": [
- "5m",
- "15m",
- "1h",
- "6h",
- "12h",
- "24h",
- "2d",
- "7d",
- "30d"
- ]
- },
- "timezone": "",
- "title": "Agent Operational",
- "uid": "",
- "version": 0
-}
diff --git a/example/weather/docker-compose/grafana/dashboards/agent-remote-write.json b/example/weather/docker-compose/grafana/dashboards/agent-remote-write.json
deleted file mode 100644
index f2227746..00000000
--- a/example/weather/docker-compose/grafana/dashboards/agent-remote-write.json
+++ /dev/null
@@ -1,1512 +0,0 @@
-{
- "__inputs": [ ],
- "__requires": [ ],
- "annotations": {
- "list": [ ]
- },
- "editable": true,
- "gnetId": null,
- "graphTooltip": 0,
- "hideControls": false,
- "id": null,
- "links": [ ],
- "refresh": "30s",
- "rows": [
- {
- "collapse": false,
- "collapsed": false,
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 2,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "(\n prometheus_remote_storage_highest_timestamp_in_seconds{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}\n -\n ignoring(url, remote_name) group_right(pod)\n prometheus_remote_storage_queue_highest_sent_timestamp_seconds{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}\n)\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Highest Timestamp In vs. Highest Timestamp Sent",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 3,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(prometheus_remote_storage_sent_batch_duration_seconds_sum{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[1m]) / rate(prometheus_remote_storage_sent_batch_duration_seconds_count{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[1m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "mean {{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- },
- {
- "expr": "histogram_quantile(0.99, rate(prometheus_remote_storage_sent_batch_duration_seconds_bucket{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[1m]))",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "p99 {{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "B"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Latency [1m]",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Timestamps",
- "titleSize": "h6",
- "type": "row"
- },
- {
- "collapse": false,
- "collapsed": false,
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 4,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(agent_wal_samples_appended_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Rate in [5m]",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 5,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(prometheus_remote_storage_succeeded_samples_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m]) or rate(prometheus_remote_storage_samples_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Rate succeeded [5m]",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 6,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "prometheus_remote_storage_samples_pending{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Pending Samples",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 7,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(prometheus_remote_storage_samples_dropped_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Dropped Samples",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 8,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(prometheus_remote_storage_samples_failed_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Failed Samples",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 9,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(prometheus_remote_storage_samples_retried_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Retried Samples",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Samples",
- "titleSize": "h6",
- "type": "row"
- },
- {
- "collapse": false,
- "collapsed": false,
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 10,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "minSpan": 6,
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 12,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "prometheus_remote_storage_shards{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Current Shards",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 11,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 4,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "prometheus_remote_storage_shards_max{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Max Shards",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 12,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 4,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "prometheus_remote_storage_shards_min{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Min Shards",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 13,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 4,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "prometheus_remote_storage_shards_desired{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Desired Shards",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Shards",
- "titleSize": "h6",
- "type": "row"
- },
- {
- "collapse": false,
- "collapsed": false,
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 14,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "prometheus_remote_storage_shard_capacity{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Shard Capacity",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Shard Details",
- "titleSize": "h6",
- "type": "row"
- },
- {
- "collapse": false,
- "collapsed": false,
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 15,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "prometheus_wal_watcher_current_segment{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Remote Write Current Segment",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "none",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Segments",
- "titleSize": "h6",
- "type": "row"
- },
- {
- "collapse": false,
- "collapsed": false,
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 16,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(prometheus_remote_storage_enqueue_retries_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m])",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{cluster}}:{{pod}}-{{instance_group_name}}-{{url}}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Enqueue Retries",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Misc. Rates",
- "titleSize": "h6",
- "type": "row"
- }
- ],
- "schemaVersion": 14,
- "style": "dark",
- "tags": [
- "grafana-agent-mixin"
- ],
- "templating": {
- "list": [
- {
- "hide": 0,
- "label": null,
- "name": "datasource",
- "options": [ ],
- "query": "prometheus",
- "refresh": 1,
- "regex": "",
- "type": "datasource"
- },
- {
- "allValue": null,
- "current": {
- "text": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "value": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- }
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": null,
- "multi": false,
- "name": "cluster",
- "options": [ ],
- "query": "label_values(agent_build_info, cluster)",
- "refresh": 2,
- "regex": "",
- "sort": 0,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": null,
- "current": {
- "text": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "value": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- }
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": null,
- "multi": false,
- "name": "namespace",
- "options": [ ],
- "query": "label_values(agent_build_info, namespace)",
- "refresh": 2,
- "regex": "",
- "sort": 0,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": null,
- "current": {
- "text": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "value": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- }
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": null,
- "multi": false,
- "name": "container",
- "options": [ ],
- "query": "label_values(agent_build_info, container)",
- "refresh": 2,
- "regex": "",
- "sort": 0,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": null,
- "current": {
- "text": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "value": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- }
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": null,
- "multi": false,
- "name": "pod",
- "options": [ ],
- "query": "label_values(agent_build_info{container=~\"$container\"}, pod)",
- "refresh": 2,
- "regex": "",
- "sort": 0,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": null,
- "current": { },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": null,
- "multi": false,
- "name": "url",
- "options": [ ],
- "query": "label_values(prometheus_remote_storage_shards{cluster=~\"$cluster\", pod=~\"$pod\"}, url)",
- "refresh": 2,
- "regex": "",
- "sort": 0,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- }
- ]
- },
- "time": {
- "from": "now-1h",
- "to": "now"
- },
- "timepicker": {
- "refresh_intervals": [
- "5s",
- "10s",
- "30s",
- "1m",
- "5m",
- "15m",
- "30m",
- "1h",
- "2h",
- "1d"
- ],
- "time_options": [
- "5m",
- "15m",
- "1h",
- "6h",
- "12h",
- "24h",
- "2d",
- "7d",
- "30d"
- ]
- },
- "timezone": "",
- "title": "Agent Prometheus Remote Write",
- "version": 0
-}
diff --git a/example/weather/docker-compose/grafana/dashboards/agent-tracing-pipeline.json b/example/weather/docker-compose/grafana/dashboards/agent-tracing-pipeline.json
deleted file mode 100644
index 3e9415b9..00000000
--- a/example/weather/docker-compose/grafana/dashboards/agent-tracing-pipeline.json
+++ /dev/null
@@ -1,1065 +0,0 @@
-{
- "__inputs": [ ],
- "__requires": [ ],
- "annotations": {
- "list": [ ]
- },
- "editable": true,
- "gnetId": null,
- "graphTooltip": 0,
- "hideControls": false,
- "id": null,
- "links": [ ],
- "refresh": "30s",
- "rows": [
- {
- "collapse": false,
- "collapsed": false,
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 0,
- "fillGradient": 0,
- "gridPos": { },
- "id": 2,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": false,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(traces_receiver_accepted_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",receiver!=\"otlp/lb\"}[$__rate_interval])\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{ pod }} - {{ receiver }}/{{ transport }}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Accepted spans",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 0,
- "fillGradient": 0,
- "gridPos": { },
- "id": 3,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": false,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(traces_receiver_refused_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",receiver!=\"otlp/lb\"}[$__rate_interval])\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{ pod }} - {{ receiver }}/{{ transport }}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Refused spans",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 0,
- "fillGradient": 0,
- "gridPos": { },
- "id": 4,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": false,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(traces_exporter_sent_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",exporter!=\"otlp\"}[$__rate_interval])\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{ pod }} - {{ exporter }}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Exported spans",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 0,
- "fillGradient": 0,
- "gridPos": { },
- "id": 5,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": false,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(traces_exporter_send_failed_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",exporter!=\"otlp\"}[$__rate_interval])\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{ pod }} - {{ exporter }}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Exported failed spans",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 6,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum(rate(traces_receiver_accepted_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",receiver!=\"otlp/lb\"}[$__rate_interval]))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "Accepted",
- "refId": "A"
- },
- {
- "expr": "sum(rate(traces_receiver_refused_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",receiver!=\"otlp/lb\"}[$__rate_interval]))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "Refused",
- "refId": "B"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Received spans",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 7,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum(rate(traces_exporter_sent_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",exporter!=\"otlp\"}[$__rate_interval]))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "Sent",
- "refId": "A"
- },
- {
- "expr": "sum(rate(traces_exporter_send_failed_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",exporter!=\"otlp\"}[$__rate_interval]))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "Send failed",
- "refId": "B"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Exported spans",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Write / Read",
- "titleSize": "h6",
- "type": "row"
- },
- {
- "collapse": false,
- "collapsed": false,
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 8,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 3,
- "stack": true,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(traces_loadbalancer_backend_outcome{cluster=~\"$cluster\",namespace=~\"$namespace\",success=\"true\",container=~\"$container\",pod=~\"$pod\"}[$__rate_interval])\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{ pod }}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Load-balanced spans",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 0,
- "fillGradient": 0,
- "gridPos": { },
- "id": 9,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": false,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "traces_loadbalancer_num_backends{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\"}\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{ pod }}",
- "refId": "A"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Number of peers",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 10,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum(rate(traces_receiver_accepted_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",receiver=\"otlp/lb\"}[$__rate_interval]))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "Accepted",
- "refId": "A"
- },
- {
- "expr": "sum(rate(traces_receiver_refused_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",receiver=\"otlp/lb\"}[$__rate_interval]))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "Refused",
- "refId": "B"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Received spans",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { },
- "id": 11,
- "interval": "1m",
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "sideWidth": null,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "repeat": null,
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 3,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum(rate(traces_exporter_sent_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",exporter=\"otlp\"}[$__rate_interval]))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "Sent",
- "refId": "A"
- },
- {
- "expr": "sum(rate(traces_exporter_send_failed_spans{cluster=~\"$cluster\",namespace=~\"$namespace\",container=~\"$container\",pod=~\"$pod\",exporter=\"otlp\"}[$__rate_interval]))\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "Send failed",
- "refId": "B"
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Exported spans",
- "tooltip": {
- "shared": true,
- "sort": 0,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": true
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Load balancing",
- "titleSize": "h6",
- "type": "row"
- }
- ],
- "schemaVersion": 14,
- "style": "dark",
- "tags": [
- "grafana-agent-mixin"
- ],
- "templating": {
- "list": [
- {
- "hide": 0,
- "label": null,
- "name": "datasource",
- "options": [ ],
- "query": "prometheus",
- "refresh": 1,
- "regex": "",
- "type": "datasource"
- },
- {
- "allValue": null,
- "current": {
- "text": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "value": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- }
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": null,
- "multi": false,
- "name": "cluster",
- "options": [ ],
- "query": "label_values(agent_build_info, cluster)",
- "refresh": 2,
- "regex": "",
- "sort": 0,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": null,
- "current": {
- "text": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "value": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- }
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": null,
- "multi": false,
- "name": "namespace",
- "options": [ ],
- "query": "label_values(agent_build_info, namespace)",
- "refresh": 2,
- "regex": "",
- "sort": 0,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": null,
- "current": {
- "text": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "value": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- }
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": null,
- "multi": false,
- "name": "container",
- "options": [ ],
- "query": "label_values(agent_build_info, container)",
- "refresh": 2,
- "regex": "",
- "sort": 0,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": null,
- "current": {
- "text": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "value": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- }
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": null,
- "multi": false,
- "name": "pod",
- "options": [ ],
- "query": "label_values(agent_build_info{container=~\"$container\"}, pod)",
- "refresh": 2,
- "regex": "",
- "sort": 0,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- }
- ]
- },
- "time": {
- "from": "now-1h",
- "to": "now"
- },
- "timepicker": {
- "refresh_intervals": [
- "5s",
- "10s",
- "30s",
- "1m",
- "5m",
- "15m",
- "30m",
- "1h",
- "2h",
- "1d"
- ],
- "time_options": [
- "5m",
- "15m",
- "1h",
- "6h",
- "12h",
- "24h",
- "2d",
- "7d",
- "30d"
- ]
- },
- "timezone": "",
- "title": "Agent Tracing Pipeline",
- "version": 0
-}
diff --git a/example/weather/docker-compose/grafana/dashboards/agent.json b/example/weather/docker-compose/grafana/dashboards/agent.json
deleted file mode 100644
index 768fccb0..00000000
--- a/example/weather/docker-compose/grafana/dashboards/agent.json
+++ /dev/null
@@ -1,786 +0,0 @@
-{
- "annotations": {
- "list": [ ]
- },
- "editable": true,
- "gnetId": null,
- "graphTooltip": 0,
- "hideControls": false,
- "links": [ ],
- "refresh": "30s",
- "rows": [
- {
- "collapse": false,
- "height": "250px",
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 1,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 12,
- "stack": false,
- "steppedLine": false,
- "styles": [
- {
- "alias": "Time",
- "dateFormat": "YYYY-MM-DD HH:mm:ss",
- "pattern": "Time",
- "type": "hidden"
- },
- {
- "alias": "Count",
- "colorMode": null,
- "colors": [ ],
- "dateFormat": "YYYY-MM-DD HH:mm:ss",
- "decimals": 2,
- "link": false,
- "linkTargetBlank": false,
- "linkTooltip": "Drill down",
- "linkUrl": "",
- "pattern": "Value #A",
- "thresholds": [ ],
- "type": "hidden",
- "unit": "short"
- },
- {
- "alias": "Uptime",
- "colorMode": null,
- "colors": [ ],
- "dateFormat": "YYYY-MM-DD HH:mm:ss",
- "decimals": 2,
- "link": false,
- "linkTargetBlank": false,
- "linkTooltip": "Drill down",
- "linkUrl": "",
- "pattern": "Value #B",
- "thresholds": [ ],
- "type": "number",
- "unit": "short"
- },
- {
- "alias": "Container",
- "colorMode": null,
- "colors": [ ],
- "dateFormat": "YYYY-MM-DD HH:mm:ss",
- "decimals": 2,
- "link": false,
- "linkTargetBlank": false,
- "linkTooltip": "Drill down",
- "linkUrl": "",
- "pattern": "container",
- "thresholds": [ ],
- "type": "number",
- "unit": "short"
- },
- {
- "alias": "Pod",
- "colorMode": null,
- "colors": [ ],
- "dateFormat": "YYYY-MM-DD HH:mm:ss",
- "decimals": 2,
- "link": false,
- "linkTargetBlank": false,
- "linkTooltip": "Drill down",
- "linkUrl": "",
- "pattern": "pod",
- "thresholds": [ ],
- "type": "number",
- "unit": "short"
- },
- {
- "alias": "Version",
- "colorMode": null,
- "colors": [ ],
- "dateFormat": "YYYY-MM-DD HH:mm:ss",
- "decimals": 2,
- "link": false,
- "linkTargetBlank": false,
- "linkTooltip": "Drill down",
- "linkUrl": "",
- "pattern": "version",
- "thresholds": [ ],
- "type": "number",
- "unit": "short"
- },
- {
- "alias": "",
- "colorMode": null,
- "colors": [ ],
- "dateFormat": "YYYY-MM-DD HH:mm:ss",
- "decimals": 2,
- "pattern": "/.*/",
- "thresholds": [ ],
- "type": "string",
- "unit": "short"
- }
- ],
- "targets": [
- {
- "expr": "count by (pod, container, version) (agent_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"})",
- "format": "table",
- "instant": true,
- "intervalFactor": 2,
- "legendFormat": "",
- "refId": "A",
- "step": 10
- },
- {
- "expr": "max by (pod, container) (time() - process_start_time_seconds{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"})",
- "format": "table",
- "instant": true,
- "intervalFactor": 2,
- "legendFormat": "",
- "refId": "B",
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Agent Stats",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "transform": "table",
- "type": "table",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Agent Stats",
- "titleSize": "h6"
- },
- {
- "collapse": false,
- "height": "250px",
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 2,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum(rate(prometheus_target_sync_length_seconds_sum{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m])) by (pod, scrape_job) * 1e3",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}/{{scrape_job}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Target Sync",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "ms",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 10,
- "id": 3,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 0,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 6,
- "stack": true,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum by (pod) (prometheus_sd_discovered_targets{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"})",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Targets",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Prometheus Discovery",
- "titleSize": "h6"
- },
- {
- "collapse": false,
- "height": "250px",
- "panels": [
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 1,
- "id": 4,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 1,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 4,
- "stack": false,
- "steppedLine": false,
- "targets": [
- {
- "expr": "rate(prometheus_target_interval_length_seconds_sum{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m])\n/\nrate(prometheus_target_interval_length_seconds_count{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m])\n* 1e3\n",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{pod}} {{interval}} configured",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Average Scrape Interval Duration",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "ms",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 10,
- "id": 5,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 0,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 4,
- "stack": true,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum by (job) (rate(prometheus_target_scrapes_exceeded_sample_limit_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[1m]))",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "exceeded sample limit: {{job}}",
- "legendLink": null,
- "step": 10
- },
- {
- "expr": "sum by (job) (rate(prometheus_target_scrapes_sample_duplicate_timestamp_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[1m]))",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "duplicate timestamp: {{job}}",
- "legendLink": null,
- "step": 10
- },
- {
- "expr": "sum by (job) (rate(prometheus_target_scrapes_sample_out_of_bounds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[1m]))",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "out of bounds: {{job}}",
- "legendLink": null,
- "step": 10
- },
- {
- "expr": "sum by (job) (rate(prometheus_target_scrapes_sample_out_of_order_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[1m]))",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "out of order: {{job}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Scrape failures",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- },
- {
- "aliasColors": { },
- "bars": false,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "fill": 10,
- "id": 6,
- "legend": {
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "show": true,
- "total": false,
- "values": false
- },
- "lines": true,
- "linewidth": 0,
- "links": [ ],
- "nullPointMode": "null as zero",
- "percentage": false,
- "pointradius": 5,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [ ],
- "spaceLength": 10,
- "span": 4,
- "stack": true,
- "steppedLine": false,
- "targets": [
- {
- "expr": "sum by (job, instance_group_name) (rate(agent_wal_samples_appended_total{cluster=~\"$cluster\", namespace=~\"$namespace\", container=~\"$container\"}[5m]))",
- "format": "time_series",
- "intervalFactor": 2,
- "legendFormat": "{{job}} {{instance_group_name}}",
- "legendLink": null,
- "step": 10
- }
- ],
- "thresholds": [ ],
- "timeFrom": null,
- "timeShift": null,
- "title": "Appended Samples",
- "tooltip": {
- "shared": true,
- "sort": 2,
- "value_type": "individual"
- },
- "type": "graph",
- "xaxis": {
- "buckets": null,
- "mode": "time",
- "name": null,
- "show": true,
- "values": [ ]
- },
- "yaxes": [
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": 0,
- "show": true
- },
- {
- "format": "short",
- "label": null,
- "logBase": 1,
- "max": null,
- "min": null,
- "show": false
- }
- ]
- }
- ],
- "repeat": null,
- "repeatIteration": null,
- "repeatRowId": null,
- "showTitle": true,
- "title": "Prometheus Retrieval",
- "titleSize": "h6"
- }
- ],
- "schemaVersion": 14,
- "style": "dark",
- "tags": [
- "grafana-agent-mixin"
- ],
- "templating": {
- "list": [
- {
- "current": {
- "text": "default",
- "value": "default"
- },
- "hide": 0,
- "label": "Data Source",
- "name": "datasource",
- "options": [ ],
- "query": "prometheus",
- "refresh": 1,
- "regex": "",
- "type": "datasource"
- },
- {
- "allValue": ".+",
- "current": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": "cluster",
- "multi": true,
- "name": "cluster",
- "options": [ ],
- "query": "label_values(agent_build_info, cluster)",
- "refresh": 1,
- "regex": "",
- "sort": 2,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": ".+",
- "current": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": "namespace",
- "multi": true,
- "name": "namespace",
- "options": [ ],
- "query": "label_values(agent_build_info, namespace)",
- "refresh": 1,
- "regex": "",
- "sort": 2,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": ".+",
- "current": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": "container",
- "multi": true,
- "name": "container",
- "options": [ ],
- "query": "label_values(agent_build_info, container)",
- "refresh": 1,
- "regex": "",
- "sort": 2,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- },
- {
- "allValue": "grafana-agent-.*",
- "current": {
- "selected": true,
- "text": "All",
- "value": "$__all"
- },
- "datasource": "$datasource",
- "hide": 0,
- "includeAll": true,
- "label": "pod",
- "multi": true,
- "name": "pod",
- "options": [ ],
- "query": "label_values(agent_build_info{container=~\"$container\"}, pod)",
- "refresh": 1,
- "regex": "",
- "sort": 2,
- "tagValuesQuery": "",
- "tags": [ ],
- "tagsQuery": "",
- "type": "query",
- "useTags": false
- }
- ]
- },
- "time": {
- "from": "now-1h",
- "to": "now"
- },
- "timepicker": {
- "refresh_intervals": [
- "5s",
- "10s",
- "30s",
- "1m",
- "5m",
- "15m",
- "30m",
- "1h",
- "2h",
- "1d"
- ],
- "time_options": [
- "5m",
- "15m",
- "1h",
- "6h",
- "12h",
- "24h",
- "2d",
- "7d",
- "30d"
- ]
- },
- "timezone": "",
- "title": "Agent",
- "uid": "",
- "version": 0
-}
diff --git a/example/weather/docker-compose/grafana/dashboards/template.jsonnet b/example/weather/docker-compose/grafana/dashboards/template.jsonnet
deleted file mode 100644
index edc46021..00000000
--- a/example/weather/docker-compose/grafana/dashboards/template.jsonnet
+++ /dev/null
@@ -1,14 +0,0 @@
-local agentDashboards = import 'grafana-agent-mixin/dashboards.libsonnet';
-local agentDebugging = import 'grafana-agent-mixin/debugging.libsonnet';
-
-local result = agentDashboards + agentDebugging {
- files: {
- [name]: $.grafanaDashboards[name] {
- // Use local timezone for local testing
- timezone: '',
- }
- for name in std.objectFields($.grafanaDashboards)
- },
-};
-
-result.files
diff --git a/example/weather/docker-compose/grafana/datasources/datasource.yml b/example/weather/docker-compose/grafana/datasources/datasource.yml
deleted file mode 100644
index 1eeb4855..00000000
--- a/example/weather/docker-compose/grafana/datasources/datasource.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-apiVersion: 1
-
-deleteDatasources:
- - name: Cortex
-
-datasources:
-- name: Cortex
- type: prometheus
- access: proxy
- orgId: 1
- url: http://cortex:9009/api/prom
- basicAuth: false
- isDefault: false
- version: 1
- editable: false
-- name: Tempo
- type: tempo
- access: proxy
- orgId: 1
- url: http://tempo:3200
- basicAuth: false
- isDefault: false
- version: 1
- editable: false
- apiVersion: 1
- uid: tempo
-- name: Loki
- type: loki
- access: proxy
- orgId: 1
- url: http://loki:3100
- basicAuth: false
- isDefault: false
- version: 1
- editable: false
- jsonData:
- derivedFields:
- - datasourceUid: tempo
- matcherRegex: tid=(\w+)
- name: TraceID
- url: $${__value.raw}
-
diff --git a/example/weather/docker-compose/tempo/config/tempo.yaml b/example/weather/docker-compose/tempo/config/tempo.yaml
deleted file mode 100644
index c00a0248..00000000
--- a/example/weather/docker-compose/tempo/config/tempo.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-server:
- http_listen_port: 3200
- graceful_shutdown_timeout: 10s
-
-distributor:
- receivers:
- otlp:
- protocols:
- grpc:
- log_received_traces: true
-
-storage:
- trace:
- backend: local
- local:
- path: /tmp/tempo/traces
- wal:
- path: /tmp/tempo/wal
diff --git a/example/weather/go.mod b/example/weather/go.mod
index d83d0886..06e6e550 100644
--- a/example/weather/go.mod
+++ b/example/weather/go.mod
@@ -2,59 +2,55 @@ module goa.design/clue/example/weather
go 1.21
-replace goa.design/clue => ../../
-
require (
github.com/stretchr/testify v1.8.4
- goa.design/clue v0.18.2
- goa.design/goa/v3 v3.14.0
- goa.design/model v1.9.0
- golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
- google.golang.org/grpc v1.59.0
- google.golang.org/protobuf v1.31.0
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1
+ go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1
+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
+ goa.design/clue v0.20.0
+ goa.design/goa/v3 v3.14.2
+ goa.design/model v1.9.1
+ golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc
+ google.golang.org/grpc v1.60.1
+ google.golang.org/protobuf v1.32.0
)
require (
github.com/AnatolyRugalev/goregen v0.1.0 // indirect
- github.com/aws/smithy-go v1.17.0 // indirect
- github.com/beorn7/perks v1.0.1 // indirect
+ github.com/aws/smithy-go v1.19.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
- github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
- github.com/go-chi/chi/v5 v5.0.10 // indirect
+ github.com/go-chi/chi/v5 v5.0.11 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
- github.com/go-logr/logr v1.3.0 // indirect
+ github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
- github.com/google/uuid v1.4.0 // indirect
+ github.com/google/uuid v1.5.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect
- github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/client_golang v1.17.0 // indirect
- github.com/prometheus/client_model v0.5.0 // indirect
- github.com/prometheus/common v0.45.0 // indirect
- github.com/prometheus/procfs v0.12.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/mod v0.14.0 // indirect
- golang.org/x/net v0.18.0 // indirect
- golang.org/x/sys v0.14.0 // indirect
- golang.org/x/term v0.14.0 // indirect
+ golang.org/x/net v0.19.0 // indirect
+ golang.org/x/sys v0.16.0 // indirect
+ golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
- golang.org/x/tools v0.15.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
+ golang.org/x/tools v0.16.1 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+
+replace goa.design/clue => ../../
diff --git a/example/weather/go.sum b/example/weather/go.sum
index b1d9273c..ebb8d943 100644
--- a/example/weather/go.sum
+++ b/example/weather/go.sum
@@ -4,14 +4,10 @@ cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGB
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
github.com/AnatolyRugalev/goregen v0.1.0 h1:xrdXkLaskMnbxW0x4FWNj2yoednv0X2bcTBWpuJGYfE=
github.com/AnatolyRugalev/goregen v0.1.0/go.mod h1:sVlY1tjcirqLBRZnCcIq1+7/Lwmqz5g7IK8AStjOVzI=
-github.com/aws/smithy-go v1.17.0 h1:wWJD7LX6PBV6etBUwO0zElG0nWN9rUhp0WdYeHSHAaI=
-github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
-github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
+github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -23,13 +19,13 @@ github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBF
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
-github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
+github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
-github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@@ -38,12 +34,12 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
-github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -57,85 +53,83 @@ github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqC
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s=
github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b h1:3E44bLeN8uKYdfQqVQycPnaVviZdBLbizFhU49mtbe4=
github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b/go.mod h1:Bj8LjjP0ReT1eKt5QlKjwgi5AFm5mI6O1A2G4ChI0Ag=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
-github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
-github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
-github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
-github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
-github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
-github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
+go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
+go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgYCza3PXRUGEyCB++y1sAqm6guWFesk=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
+go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-goa.design/goa/v3 v3.14.0 h1:Ymm6hDyxWFiYclVHcMs1cl7vgSHiYLSIweHE74EusoA=
-goa.design/goa/v3 v3.14.0/go.mod h1:QYVl5438/92SiqcIzYpIwz10QAocAJeacQu+FO0lTXQ=
-goa.design/model v1.9.0 h1:EUERzWyBMnLQzVw6AtYGIM7pLan9IXhcvWxTx/0khOA=
-goa.design/model v1.9.0/go.mod h1:OQtQGJ9rp9TNzpk6gSA066538zgPM9duLz9iroG5EJ4=
-golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
-golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
+goa.design/goa/v3 v3.14.2 h1:V8skSFWRUTddqZnRy65Mdx0SpGmYe9aQhgCQppyvCKA=
+goa.design/goa/v3 v3.14.2/go.mod h1:sq7F2y/2/eK/XCc8Ff7Was3u42fRmQXp1pTm8FfnGgo=
+goa.design/model v1.9.1 h1:vhY89pjUWW1firAHaccW9MZTiMEEVvtcwN9g8odyywQ=
+goa.design/model v1.9.1/go.mod h1:PlrrmVbrBm7gxqoWqburVUudaMd/d3Sdul7/Kr4Ap54=
+golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
+golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
-golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
-golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
-golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
-golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
-golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
-golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
+golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
+golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 h1:I6WNifs6pF9tNdSob2W24JtyxIYjzFB9qDlpUC76q+U=
-google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4=
-google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
-google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
-google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
-google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
+google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
+google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
+google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
diff --git a/example/weather/images/signoz.png b/example/weather/images/signoz.png
new file mode 100644
index 00000000..0b089a8f
Binary files /dev/null and b/example/weather/images/signoz.png differ
diff --git a/example/weather/images/tempo.png b/example/weather/images/tempo.png
deleted file mode 100644
index 1fa0beff..00000000
Binary files a/example/weather/images/tempo.png and /dev/null differ
diff --git a/example/weather/scripts/load b/example/weather/scripts/load
new file mode 100755
index 00000000..e3be863c
--- /dev/null
+++ b/example/weather/scripts/load
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# Total duration for the script to run: 10 minutes (600 seconds)
+DURATION=600
+
+# Start time
+START_TIME=$(date +%s)
+
+# End time = Start time + Duration
+END_TIME=$((START_TIME + DURATION))
+
+# Loop to run until the current time is less than the end time
+while [ $(date +%s) -lt $END_TIME ]; do
+ # Run the command
+ http localhost:8084/forecast/142.250.68.14
+
+ # Wait for 1 second before the next iteration
+ sleep 1
+done
+
diff --git a/example/weather/scripts/server b/example/weather/scripts/server
index fa4cf93e..af5f478a 100755
--- a/example/weather/scripts/server
+++ b/example/weather/scripts/server
@@ -21,7 +21,7 @@ else
export HOST_GATEWAY='host-gateway'
fi
-docker-compose -f docker-compose/docker-compose-grafana.yaml up -d
+docker-compose -f signoz/deploy/docker/clickhouse-setup/docker-compose.yaml up -d
overmind start
diff --git a/example/weather/scripts/setup b/example/weather/scripts/setup
index fa73e992..2de51817 100755
--- a/example/weather/scripts/setup
+++ b/example/weather/scripts/setup
@@ -31,7 +31,6 @@ if [[ "$CI" == "" ]]; then
fi
go mod download
-go install honnef.co/go/tools/cmd/staticcheck@latest
go install goa.design/clue/mock/cmd/cmg@latest
go install goa.design/model/cmd/mdl@latest
go install goa.design/goa/v3/...@v3
@@ -39,6 +38,13 @@ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0
go install github.com/DarthSim/overmind/v2@latest
+# clone SigNoz for docker compose files
+if [[ ! -d "signoz" ]]; then
+ git clone --depth 1 -b main https://github.com/SigNoz/signoz.git
+ # remove the hotrod example
+ sed -i.bak '/hotrod:/,$d' signoz/deploy/docker/clickhouse-setup/docker-compose.yaml
+fi
+
./scripts/build
popd
diff --git a/example/weather/services/forecaster/cmd/forecaster/main.go b/example/weather/services/forecaster/cmd/forecaster/main.go
index e25f79d4..fad4a81f 100644
--- a/example/weather/services/forecaster/cmd/forecaster/main.go
+++ b/example/weather/services/forecaster/cmd/forecaster/main.go
@@ -6,18 +6,22 @@ import (
"fmt"
"net"
"net/http"
+ "net/http/httptrace"
"os"
"os/signal"
"sync"
"syscall"
"time"
+ "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
+ "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
+ "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+ "goa.design/clue/clue"
"goa.design/clue/debug"
"goa.design/clue/health"
"goa.design/clue/log"
- "goa.design/clue/metrics"
- "goa.design/clue/trace"
- goagrpcmiddleware "goa.design/goa/v3/grpc/middleware"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/reflection"
@@ -31,11 +35,10 @@ import (
func main() {
var (
- grpcaddr = flag.String("grpc-addr", ":8080", "gRPC listen address")
- httpaddr = flag.String("http-addr", ":8081", "HTTP listen address (health checks and metrics)")
- agentaddr = flag.String("agent-addr", ":4317", "Grafana agent listen address")
- debugf = flag.Bool("debug", false, "Enable debug logs")
- monitoringEnabled = flag.Bool("monitoring-enabled", true, "monitoring")
+ grpcaddr = flag.String("grpc-addr", ":8080", "gRPC listen address")
+ httpaddr = flag.String("http-addr", ":8081", "HTTP listen address (health checks and metrics)")
+ coladdr = flag.String("otel-addr", ":4317", "OpenTelemetry collector listen address")
+ debugf = flag.Bool("debug", false, "Enable debug logs")
)
flag.Parse()
@@ -44,60 +47,70 @@ func main() {
if log.IsTerminal() {
format = log.FormatTerminal
}
- ctx := log.Context(context.Background(), log.WithFormat(format), log.WithFunc(trace.Log))
- ctx = log.With(ctx, log.KV{K: "svc", V: genforecaster.ServiceName})
+ ctx := log.Context(context.Background(), log.WithFormat(format), log.WithFunc(log.Span))
if *debugf {
ctx = log.Context(ctx, log.WithDebug())
log.Debugf(ctx, "debug logs enabled")
}
- // 2. Setup tracing
- if !*monitoringEnabled {
- var err error
- if ctx, err = trace.Context(ctx, genforecaster.ServiceName, trace.WithDisabled()); err != nil {
- log.Error(ctx, err, log.KV{K: "msg", V: "failed to initialize tracing"})
- }
- } else {
- log.Debugf(ctx, "connecting to Grafana agent %s", *agentaddr)
- conn, err := grpc.DialContext(ctx, *agentaddr,
- grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithBlock())
- if err != nil {
- log.Errorf(ctx, err, "failed to connect to Grafana agent")
- os.Exit(1)
+ // 2. Setup instrumentation
+ spanExporter, err := otlptracegrpc.New(ctx,
+ otlptracegrpc.WithEndpoint(*coladdr),
+ otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize tracing")
+ }
+ defer func() {
+ if err := spanExporter.Shutdown(ctx); err != nil {
+ log.Errorf(ctx, err, "failed to shutdown tracing")
}
- log.Debugf(ctx, "connected to Grafana agent %s", *agentaddr)
- ctx, err = trace.Context(ctx, genforecaster.ServiceName, trace.WithGRPCExporter(conn))
- if err != nil {
- log.Errorf(ctx, err, "failed to initialize tracing")
- os.Exit(1)
+ }()
+ metricExporter, err := otlpmetricgrpc.New(ctx,
+ otlpmetricgrpc.WithEndpoint(*coladdr),
+ otlpmetricgrpc.WithTLSCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize metrics")
+ }
+ defer func() {
+ if err := metricExporter.Shutdown(ctx); err != nil {
+ log.Errorf(ctx, err, "failed to shutdown metrics")
}
+ }()
+ cfg, err := clue.NewConfig(ctx,
+ genforecaster.ServiceName,
+ genforecaster.APIVersion,
+ metricExporter,
+ spanExporter,
+ )
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize instrumentation")
}
-
- // 3. Setup metrics
- ctx = metrics.Context(ctx, genforecaster.ServiceName)
-
- // 4. Create clients
- c := &http.Client{Transport: log.Client(trace.Client(ctx, http.DefaultTransport))}
- wc := weathergov.New(c)
-
- // 5. Create service & endpoints
+ clue.ConfigureOpenTelemetry(ctx, cfg)
+
+ // 3. Create clients
+ httpc := &http.Client{
+ Transport: log.Client(
+ otelhttp.NewTransport(
+ http.DefaultTransport,
+ otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
+ return otelhttptrace.NewClientTrace(ctx)
+ }),
+ ))}
+ wc := weathergov.New(httpc)
+
+ // 4. Create service & endpoints
svc := forecaster.New(wc)
endpoints := genforecaster.NewEndpoints(svc)
endpoints.Use(debug.LogPayloads())
endpoints.Use(log.Endpoint)
- // 6. Create transport
+ // 5. Create transport
server := gengrpc.New(endpoints, nil)
grpcsvr := grpc.NewServer(
grpc.ChainUnaryInterceptor(
- goagrpcmiddleware.UnaryRequestID(),
- log.UnaryServerInterceptor(ctx),
- debug.UnaryServerInterceptor(),
- goagrpcmiddleware.UnaryServerLogContext(log.AsGoaMiddlewareLogger),
- metrics.UnaryServerInterceptor(ctx),
- trace.UnaryServerInterceptor(ctx),
- ))
+ log.UnaryServerInterceptor(ctx), // Add logger to request context and log requests.
+ debug.UnaryServerInterceptor()), // Enable debug log level control
+ grpc.StatsHandler(otelgrpc.NewServerHandler())) // Instrument server.
genpb.RegisterForecasterServer(grpcsvr, server)
reflection.Register(grpcsvr)
for svc, info := range grpcsvr.GetServiceInfo() {
@@ -106,17 +119,17 @@ func main() {
}
}
- // 7. Setup health check, metrics and debug endpoints
- check := log.HTTP(ctx)(health.Handler(health.NewChecker(wc)))
+ // 6. Setup health check and debug endpoints
mux := http.NewServeMux()
debug.MountDebugLogEnabler(mux)
debug.MountPprofHandlers(mux)
+ check := health.Handler(health.NewChecker(wc))
+ check = log.HTTP(ctx)(check).(http.HandlerFunc) // Log health-check errors
mux.Handle("/healthz", check)
mux.Handle("/livez", check)
- mux.Handle("/metrics", metrics.Handler(ctx))
httpsvr := &http.Server{Addr: *httpaddr, Handler: mux}
- // 8. Start gRPC and HTTP servers
+ // 7. Start gRPC and HTTP servers
errc := make(chan error)
go func() {
c := make(chan os.Signal, 1)
diff --git a/example/weather/services/forecaster/design/design.go b/example/weather/services/forecaster/design/design.go
index a6e6375a..a6ad9e37 100644
--- a/example/weather/services/forecaster/design/design.go
+++ b/example/weather/services/forecaster/design/design.go
@@ -9,6 +9,7 @@ import (
var _ = API("Weather Service API", func() {
Title("The Weather Service API")
Description("A fully instrumented weather service API")
+ Version("1.0.0")
})
var _ = Service("Forecaster", func() {
diff --git a/example/weather/services/forecaster/gen/forecaster/client.go b/example/weather/services/forecaster/gen/forecaster/client.go
index de3bd3f9..cca41925 100644
--- a/example/weather/services/forecaster/gen/forecaster/client.go
+++ b/example/weather/services/forecaster/gen/forecaster/client.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster client
//
diff --git a/example/weather/services/forecaster/gen/forecaster/endpoints.go b/example/weather/services/forecaster/gen/forecaster/endpoints.go
index e8c28726..c5149312 100644
--- a/example/weather/services/forecaster/gen/forecaster/endpoints.go
+++ b/example/weather/services/forecaster/gen/forecaster/endpoints.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster endpoints
//
diff --git a/example/weather/services/forecaster/gen/forecaster/service.go b/example/weather/services/forecaster/gen/forecaster/service.go
index 23c8995d..59e73113 100644
--- a/example/weather/services/forecaster/gen/forecaster/service.go
+++ b/example/weather/services/forecaster/gen/forecaster/service.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster service
//
@@ -23,6 +23,9 @@ type Service interface {
// key.
const ServiceName = "Forecaster"
+// APIVersion is the version of the API as defined in the design.
+const APIVersion = "1.0.0"
+
// MethodNames lists the service method names as defined in the design. These
// are the same values that are set in the endpoint request contexts under the
// MethodKey key.
diff --git a/example/weather/services/forecaster/gen/grpc/cli/weather_service_api/cli.go b/example/weather/services/forecaster/gen/grpc/cli/weather_service_api/cli.go
index 3b2a26c1..24ea3ad7 100644
--- a/example/weather/services/forecaster/gen/grpc/cli/weather_service_api/cli.go
+++ b/example/weather/services/forecaster/gen/grpc/cli/weather_service_api/cli.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Weather Service API gRPC client CLI support package
//
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/client/cli.go b/example/weather/services/forecaster/gen/grpc/forecaster/client/cli.go
index 586d9ef8..0edca659 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/client/cli.go
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/client/cli.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster gRPC client CLI support package
//
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/client/client.go b/example/weather/services/forecaster/gen/grpc/forecaster/client/client.go
index c05d4235..1cf9d4e5 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/client/client.go
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/client/client.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster gRPC client
//
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/client/encode_decode.go b/example/weather/services/forecaster/gen/grpc/forecaster/client/encode_decode.go
index 6ffdb13c..3469bc8b 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/client/encode_decode.go
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/client/encode_decode.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster gRPC client encoders and decoders
//
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/client/types.go b/example/weather/services/forecaster/gen/grpc/forecaster/client/types.go
index cab4ad9f..d1e24c5d 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/client/types.go
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/client/types.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster gRPC client types
//
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster.pb.go b/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster.pb.go
index 829c2e86..ef302b4e 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster.pb.go
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster.pb.go
@@ -1,4 +1,4 @@
-// Code generated with goa v3.14.0, DO NOT EDIT.
+// Code generated with goa v3.14.2, DO NOT EDIT.
//
// Forecaster protocol buffer definition
//
@@ -9,7 +9,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
-// protoc v4.25.1
+// protoc v4.25.0
// source: goagen_forecaster_forecaster.proto
package forecasterpb
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster.proto b/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster.proto
index 267db1c6..3157928d 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster.proto
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster.proto
@@ -1,4 +1,4 @@
-// Code generated with goa v3.14.0, DO NOT EDIT.
+// Code generated with goa v3.14.2, DO NOT EDIT.
//
// Forecaster protocol buffer definition
//
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster_grpc.pb.go b/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster_grpc.pb.go
index 5e4d5917..4069f7d2 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster_grpc.pb.go
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/pb/goagen_forecaster_forecaster_grpc.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
-// - protoc v4.25.1
+// - protoc v4.25.0
// source: goagen_forecaster_forecaster.proto
package forecasterpb
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/server/encode_decode.go b/example/weather/services/forecaster/gen/grpc/forecaster/server/encode_decode.go
index 8a28011e..2c49d80a 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/server/encode_decode.go
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/server/encode_decode.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster gRPC server encoders and decoders
//
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/server/server.go b/example/weather/services/forecaster/gen/grpc/forecaster/server/server.go
index 48389d80..35aa117c 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/server/server.go
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/server/server.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster gRPC server
//
diff --git a/example/weather/services/forecaster/gen/grpc/forecaster/server/types.go b/example/weather/services/forecaster/gen/grpc/forecaster/server/types.go
index c911dd7d..d345a11b 100644
--- a/example/weather/services/forecaster/gen/grpc/forecaster/server/types.go
+++ b/example/weather/services/forecaster/gen/grpc/forecaster/server/types.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Forecaster gRPC server types
//
diff --git a/example/weather/services/front/cmd/front/main.go b/example/weather/services/front/cmd/front/main.go
index c8d30173..d66a8431 100644
--- a/example/weather/services/front/cmd/front/main.go
+++ b/example/weather/services/front/cmd/front/main.go
@@ -11,13 +11,15 @@ import (
"syscall"
"time"
+ "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
+ "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+ "goa.design/clue/clue"
"goa.design/clue/debug"
"goa.design/clue/health"
"goa.design/clue/log"
- "goa.design/clue/metrics"
- "goa.design/clue/trace"
goahttp "goa.design/goa/v3/http"
- goahttpmiddleware "goa.design/goa/v3/http/middleware"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -37,11 +39,9 @@ func main() {
forecasterHealthAddr = flag.String("forecaster-health-addr", ":8081", "Forecaster service health-check address")
locatorAddr = flag.String("locator-addr", ":8082", "Locator service address")
locatorHealthAddr = flag.String("locator-health-addr", ":8083", "Locator service health-check address")
+ coladdr = flag.String("otel-addr", ":4317", "OpenTelemtry collector listen address")
+ debugf = flag.Bool("debug", false, "Enable debug logs")
testerAddr = flag.String("tester-addr", ":8090", "Tester service address")
- // No testerHealthAddr because we don't want the whole system to die just because tester isn't healthy for some reason
- agentaddr = flag.String("agent-addr", ":4317", "Grafana agent listen address")
- debugf = flag.Bool("debug", false, "Enable debug logs")
- monitoringEnabled = flag.Bool("monitoring-enabled", true, "monitoring")
)
flag.Parse()
@@ -50,70 +50,68 @@ func main() {
if log.IsTerminal() {
format = log.FormatTerminal
}
- ctx := log.Context(context.Background(), log.WithFormat(format), log.WithFunc(trace.Log))
- ctx = log.With(ctx, log.KV{K: "svc", V: genfront.ServiceName})
+ ctx := log.Context(context.Background(), log.WithFormat(format), log.WithFunc(log.Span))
if *debugf {
ctx = log.Context(ctx, log.WithDebug())
log.Debugf(ctx, "debug logs enabled")
}
- // 2. Setup tracing
- if !*monitoringEnabled {
- var err error
- if ctx, err = trace.Context(ctx, genfront.ServiceName, trace.WithDisabled()); err != nil {
- log.Error(ctx, err, log.KV{K: "msg", V: "failed to initialize tracing"})
- }
- } else {
- log.Debugf(ctx, "connecting to Grafana agent %s", *agentaddr)
- conn, err := grpc.DialContext(ctx, *agentaddr,
- grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithBlock())
- if err != nil {
- log.Errorf(ctx, err, "failed to connect to Grafana agent")
- os.Exit(1)
- }
- log.Debugf(ctx, "connected to Grafana agent %s", *agentaddr)
- ctx, err = trace.Context(ctx, genfront.ServiceName, trace.WithGRPCExporter(conn))
- if err != nil {
- log.Errorf(ctx, err, "failed to initialize tracing")
- os.Exit(1)
+ // 2. Setup instrumentation
+ spanExporter, err := otlptracegrpc.New(ctx,
+ otlptracegrpc.WithEndpoint(*coladdr),
+ otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize tracing")
+ }
+ defer func() {
+ if err := spanExporter.Shutdown(ctx); err != nil {
+ log.Errorf(ctx, err, "failed to shutdown tracing")
}
+ }()
+ metricExporter, err := otlpmetricgrpc.New(ctx,
+ otlpmetricgrpc.WithEndpoint(*coladdr),
+ otlpmetricgrpc.WithTLSCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize metrics")
}
-
- // 3. Setup metrics
- mux := goahttp.NewMuxer()
- ctx = metrics.Context(ctx, genfront.ServiceName,
- metrics.WithRouteResolver(func(r *http.Request) string {
- return mux.ResolvePattern(r)
- }),
+ defer func() {
+ if err := metricExporter.Shutdown(ctx); err != nil {
+ log.Errorf(ctx, err, "failed to shutdown metrics")
+ }
+ }()
+ cfg, err := clue.NewConfig(ctx,
+ genfront.ServiceName,
+ genfront.APIVersion,
+ metricExporter,
+ spanExporter,
)
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize instrumentation")
+ }
+ clue.ConfigureOpenTelemetry(ctx, cfg)
// 3. Create clients
- lcc, err := grpc.DialContext(ctx, *locatorAddr,
+ lcc, err := grpc.DialContext(ctx,
+ *locatorAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithChainUnaryInterceptor(
- trace.UnaryClientInterceptor(ctx),
- log.UnaryClientInterceptor()))
+ grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()),
+ grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
if err != nil {
- log.Errorf(ctx, err, "failed to connect to locator")
- os.Exit(1)
+ log.Fatalf(ctx, err, "failed to connect to locator")
}
lc := locator.New(lcc)
fcc, err := grpc.DialContext(ctx, *forecasterAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithChainUnaryInterceptor(
- trace.UnaryClientInterceptor(ctx),
- log.UnaryClientInterceptor()))
+ grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()),
+ grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
if err != nil {
- log.Errorf(ctx, err, "failed to connect to forecast")
- os.Exit(1)
+ log.Fatalf(ctx, err, "failed to connect to forecast")
}
fc := forecaster.New(fcc)
tcc, err := grpc.DialContext(ctx, *testerAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithChainUnaryInterceptor(
- trace.UnaryClientInterceptor(ctx),
- log.UnaryClientInterceptor()))
+ grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()),
+ grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
if err != nil {
log.Errorf(ctx, err, "failed to connect to tester")
os.Exit(1)
@@ -127,12 +125,12 @@ func main() {
endpoints.Use(log.Endpoint)
// 5. Create transport
- mux.Use(metrics.HTTP(ctx))
+ mux := goahttp.NewMuxer()
debug.MountDebugLogEnabler(debug.Adapt(mux))
- handler := trace.HTTP(ctx)(mux) // 4. Trace request
- handler = goahttpmiddleware.LogContext(log.AsGoaMiddlewareLogger)(handler) // 3. Log request and response
- handler = log.HTTP(ctx)(handler) // 2. Add logger to request context (with request ID key)
- handler = goahttpmiddleware.RequestID()(handler) // 1. Add request ID to context
+ debug.MountPprofHandlers(debug.Adapt(mux))
+ handler := otelhttp.NewHandler(mux, genfront.ServiceName) // 3. Add OpenTelemetry instrumentation
+ handler = debug.HTTP()(handler) // 2. Add debug endpoints
+ handler = log.HTTP(ctx)(handler) // 1. Add logger to request context
server := genhttp.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil)
genhttp.Mount(mux, server)
for _, m := range server.Mounts {
@@ -146,10 +144,9 @@ func main() {
check := health.Handler(health.NewChecker(
health.NewPinger("locator", *locatorHealthAddr),
health.NewPinger("forecaster", *forecasterHealthAddr)))
- check = log.HTTP(ctx)(check).(http.HandlerFunc)
+ check = log.HTTP(ctx)(check).(http.HandlerFunc) // Log health-check errors
http.Handle("/healthz", check)
http.Handle("/livez", check)
- http.Handle("/metrics", metrics.Handler(ctx).(http.HandlerFunc))
metricsServer := &http.Server{Addr: *metricsListenAddr}
// 7. Start HTTP server
diff --git a/example/weather/services/front/design/design.go b/example/weather/services/front/design/design.go
index 160c5b5b..3910c31b 100644
--- a/example/weather/services/front/design/design.go
+++ b/example/weather/services/front/design/design.go
@@ -9,10 +9,12 @@ import (
var _ = API("Weather", func() {
Title("Weather Forecast Service API")
Description("The weather forecast service API produces weather forecasts from US-based IPs. It uses IP location to find the appropriate weather station.")
+ Version("1.0.0")
})
var _ = Service("front", func() {
Description("Public HTTP frontend")
+
Method("forecast", func() {
Description("Retrieve weather forecast for given IP")
Payload(String, func() {
@@ -25,6 +27,7 @@ var _ = Service("front", func() {
Response("not_usa", StatusBadRequest)
})
})
+
Method("test_all", func() {
Description("Endpoint for running ALL API Integration Tests for the Weather System, allowing for filtering on included or excluded tests")
Payload(func() {
@@ -36,6 +39,7 @@ var _ = Service("front", func() {
POST("/tester/all")
})
})
+
Method("test_smoke", func() {
Description("Endpoint for running API Integration Tests' Smoke Tests ONLY for the Weather System")
Result(TestResults)
@@ -44,3 +48,53 @@ var _ = Service("front", func() {
})
})
})
+
+var TestResult = Type("TestResult", func() {
+ Description("Test result for a single test")
+ Field(1, "name", String, "Name of the test", func() {
+ Example("TestValidIP")
+ })
+ Field(2, "passed", Boolean, "Status of the test", func() {
+ Example(true)
+ })
+ Field(3, "error", String, "Error message if the test failed", func() {
+ Example("error getting location for valid ip: %v")
+ })
+ Field(4, "duration", Int64, "Duration of the test in ms", func() {
+ Example(1234)
+ })
+ Required("name", "passed", "duration")
+})
+
+var TestCollection = Type("TestCollection", func() {
+ Description("Collection of test results for grouping by service")
+ Field(1, "name", String, "Name of the test collection", func() {
+ Example("Locator Tests")
+ })
+ Field(2, "results", ArrayOf(TestResult), "Test results")
+ Field(3, "duration", Int64, "Duration of the tests in ms", func() {
+ Example(1234)
+ })
+ Field(4, "pass_count", Int, "Number of tests that passed", func() {
+ Example(12)
+ })
+ Field(5, "fail_count", Int, "Number of tests that failed", func() {
+ Example(1)
+ })
+ Required("name", "duration", "pass_count", "fail_count")
+})
+
+var TestResults = Type("TestResults", func() {
+ Description("Test results for the integration tests")
+ Field(1, "collections", ArrayOf(TestCollection), "Test collections")
+ Field(2, "duration", Int64, "Duration of the tests in ms", func() {
+ Example(1234)
+ })
+ Field(3, "pass_count", Int, "Number of tests that passed", func() {
+ Example(12)
+ })
+ Field(4, "fail_count", Int, "Number of tests that failed", func() {
+ Example(1)
+ })
+ Required("collections", "duration", "pass_count", "fail_count")
+})
diff --git a/example/weather/services/front/design/tester_results.go b/example/weather/services/front/design/tester_results.go
deleted file mode 100644
index a002db16..00000000
--- a/example/weather/services/front/design/tester_results.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package design
-
-import (
- . "goa.design/goa/v3/dsl"
-)
-
-var TestResult = Type("TestResult", func() {
- Description("Test result for a single test")
- Field(1, "name", String, "Name of the test", func() {
- Example("TestValidIP")
- })
- Field(2, "passed", Boolean, "Status of the test", func() {
- Example(true)
- })
- Field(3, "error", String, "Error message if the test failed", func() {
- Example("error getting location for valid ip: %v")
- })
- Field(4, "duration", Int64, "Duration of the test in ms", func() {
- Example(1234)
- })
- Required("name", "passed", "duration")
-})
-
-var TestCollection = Type("TestCollection", func() {
- Description("Collection of test results for grouping by service")
- Field(1, "name", String, "Name of the test collection", func() {
- Example("Locator Tests")
- })
- Field(2, "results", ArrayOf(TestResult), "Test results")
- Field(3, "duration", Int64, "Duration of the tests in ms", func() {
- Example(1234)
- })
- Field(4, "pass_count", Int, "Number of tests that passed", func() {
- Example(12)
- })
- Field(5, "fail_count", Int, "Number of tests that failed", func() {
- Example(1)
- })
- Required("name", "duration", "pass_count", "fail_count")
-})
-
-var TestResults = Type("TestResults", func() {
- Description("Test results for the integration tests")
- Field(1, "collections", ArrayOf(TestCollection), "Test collections")
- Field(2, "duration", Int64, "Duration of the tests in ms", func() {
- Example(1234)
- })
- Field(3, "pass_count", Int, "Number of tests that passed", func() {
- Example(12)
- })
- Field(4, "fail_count", Int, "Number of tests that failed", func() {
- Example(1)
- })
- Required("collections", "duration", "pass_count", "fail_count")
-})
diff --git a/example/weather/services/front/gen/front/client.go b/example/weather/services/front/gen/front/client.go
index 861d4373..d20f1060 100644
--- a/example/weather/services/front/gen/front/client.go
+++ b/example/weather/services/front/gen/front/client.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front client
//
diff --git a/example/weather/services/front/gen/front/endpoints.go b/example/weather/services/front/gen/front/endpoints.go
index 536a1824..cb01e9dd 100644
--- a/example/weather/services/front/gen/front/endpoints.go
+++ b/example/weather/services/front/gen/front/endpoints.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front endpoints
//
diff --git a/example/weather/services/front/gen/front/service.go b/example/weather/services/front/gen/front/service.go
index 142fadd1..b65c1de9 100644
--- a/example/weather/services/front/gen/front/service.go
+++ b/example/weather/services/front/gen/front/service.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front service
//
@@ -31,6 +31,9 @@ type Service interface {
// key.
const ServiceName = "front"
+// APIVersion is the version of the API as defined in the design.
+const APIVersion = "1.0.0"
+
// MethodNames lists the service method names as defined in the design. These
// are the same values that are set in the endpoint request contexts under the
// MethodKey key.
diff --git a/example/weather/services/front/gen/http/cli/weather/cli.go b/example/weather/services/front/gen/http/cli/weather/cli.go
index bea39597..3bb61d9b 100644
--- a/example/weather/services/front/gen/http/cli/weather/cli.go
+++ b/example/weather/services/front/gen/http/cli/weather/cli.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Weather HTTP client CLI support package
//
diff --git a/example/weather/services/front/gen/http/front/client/cli.go b/example/weather/services/front/gen/http/front/client/cli.go
index 3cfeb481..ee36ef6c 100644
--- a/example/weather/services/front/gen/http/front/client/cli.go
+++ b/example/weather/services/front/gen/http/front/client/cli.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front HTTP client CLI support package
//
diff --git a/example/weather/services/front/gen/http/front/client/client.go b/example/weather/services/front/gen/http/front/client/client.go
index 1ea8373f..6b7a0177 100644
--- a/example/weather/services/front/gen/http/front/client/client.go
+++ b/example/weather/services/front/gen/http/front/client/client.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front client HTTP transport
//
diff --git a/example/weather/services/front/gen/http/front/client/encode_decode.go b/example/weather/services/front/gen/http/front/client/encode_decode.go
index e7ef4462..961c13e9 100644
--- a/example/weather/services/front/gen/http/front/client/encode_decode.go
+++ b/example/weather/services/front/gen/http/front/client/encode_decode.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front HTTP client encoders and decoders
//
diff --git a/example/weather/services/front/gen/http/front/client/paths.go b/example/weather/services/front/gen/http/front/client/paths.go
index 68df4ad9..aaba626d 100644
--- a/example/weather/services/front/gen/http/front/client/paths.go
+++ b/example/weather/services/front/gen/http/front/client/paths.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// HTTP request path constructors for the front service.
//
diff --git a/example/weather/services/front/gen/http/front/client/types.go b/example/weather/services/front/gen/http/front/client/types.go
index dbef249e..86640fba 100644
--- a/example/weather/services/front/gen/http/front/client/types.go
+++ b/example/weather/services/front/gen/http/front/client/types.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front HTTP client types
//
diff --git a/example/weather/services/front/gen/http/front/server/encode_decode.go b/example/weather/services/front/gen/http/front/server/encode_decode.go
index aea1df2b..fe7a0c49 100644
--- a/example/weather/services/front/gen/http/front/server/encode_decode.go
+++ b/example/weather/services/front/gen/http/front/server/encode_decode.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front HTTP server encoders and decoders
//
diff --git a/example/weather/services/front/gen/http/front/server/paths.go b/example/weather/services/front/gen/http/front/server/paths.go
index 628000c6..99ae705b 100644
--- a/example/weather/services/front/gen/http/front/server/paths.go
+++ b/example/weather/services/front/gen/http/front/server/paths.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// HTTP request path constructors for the front service.
//
diff --git a/example/weather/services/front/gen/http/front/server/server.go b/example/weather/services/front/gen/http/front/server/server.go
index 6170bfe8..cc5b2320 100644
--- a/example/weather/services/front/gen/http/front/server/server.go
+++ b/example/weather/services/front/gen/http/front/server/server.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front HTTP server
//
diff --git a/example/weather/services/front/gen/http/front/server/types.go b/example/weather/services/front/gen/http/front/server/types.go
index ab57d794..3f8efc27 100644
--- a/example/weather/services/front/gen/http/front/server/types.go
+++ b/example/weather/services/front/gen/http/front/server/types.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// front HTTP server types
//
diff --git a/example/weather/services/front/gen/http/openapi.json b/example/weather/services/front/gen/http/openapi.json
index 5613fd41..518e482e 100644
--- a/example/weather/services/front/gen/http/openapi.json
+++ b/example/weather/services/front/gen/http/openapi.json
@@ -1 +1 @@
-{"swagger":"2.0","info":{"title":"Weather Forecast Service API","description":"The weather forecast service API produces weather forecasts from US-based IPs. It uses IP location to find the appropriate weather station.","version":""},"host":"localhost:80","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/forecast/{ip}":{"get":{"tags":["front"],"summary":"forecast front","description":"Retrieve weather forecast for given IP","operationId":"front#forecast","parameters":[{"name":"ip","in":"path","required":true,"type":"string","format":"ip"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/FrontForecastResponseBody","required":["location","periods"]}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/FrontForecastNotUsaResponseBody"}}},"schemes":["http"]}},"/tester/all":{"post":{"tags":["front"],"summary":"test_all front","description":"Endpoint for running ALL API Integration Tests for the Weather System, allowing for filtering on included or excluded tests","operationId":"front#test_all","parameters":[{"name":"test_all_request_body","in":"body","required":true,"schema":{"$ref":"#/definitions/FrontTestAllRequestBody"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/FrontTestAllResponseBody","required":["collections","duration","pass_count","fail_count"]}}},"schemes":["http"]}},"/tester/smoke":{"post":{"tags":["front"],"summary":"test_smoke front","description":"Endpoint for running API Integration Tests' Smoke Tests ONLY for the Weather System","operationId":"front#test_smoke","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/FrontTestSmokeResponseBody","required":["collections","duration","pass_count","fail_count"]}}},"schemes":["http"]}}},"definitions":{"FrontForecastNotUsaResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"IP address is not in the US (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"FrontForecastResponseBody":{"title":"FrontForecastResponseBody","type":"object","properties":{"location":{"$ref":"#/definitions/LocationResponseBody"},"periods":{"type":"array","items":{"$ref":"#/definitions/PeriodResponseBody"},"description":"Weather forecast periods","example":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]}},"example":{"location":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"periods":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]},"required":["location","periods"]},"FrontTestAllRequestBody":{"title":"FrontTestAllRequestBody","type":"object","properties":{"exclude":{"type":"array","items":{"type":"string","example":"Eos nihil accusamus reiciendis eligendi amet aut."},"description":"Tests to exclude","example":["Hic aperiam.","Assumenda commodi aut facilis."]},"include":{"type":"array","items":{"type":"string","example":"Corrupti dignissimos id necessitatibus consequatur a laudantium."},"description":"Tests to run","example":["Nihil voluptatum dolorem nam quisquam.","Incidunt ipsa natus."]}},"example":{"exclude":["Asperiores est.","Omnis vel quia ab dolorem qui.","Dicta fuga praesentium.","Est sit quis."],"include":["Aspernatur voluptatem et placeat deserunt.","Temporibus veritatis vel.","Voluptatem ab nulla ad in optio sed.","Tempora tenetur."]}},"FrontTestAllResponseBody":{"title":"FrontTestAllResponseBody","type":"object","properties":{"collections":{"type":"array","items":{"$ref":"#/definitions/TestCollectionResponseBody"},"description":"Test collections","example":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}]},"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"}},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12},"required":["collections","duration","pass_count","fail_count"]},"FrontTestSmokeResponseBody":{"title":"FrontTestSmokeResponseBody","type":"object","properties":{"collections":{"type":"array","items":{"$ref":"#/definitions/TestCollectionResponseBody"},"description":"Test collections","example":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}]},"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"}},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12},"required":["collections","duration","pass_count","fail_count"]},"LocationResponseBody":{"title":"LocationResponseBody","type":"object","properties":{"city":{"type":"string","description":"City","example":"San Francisco"},"lat":{"type":"number","description":"Latitude","example":37.8267,"format":"double"},"long":{"type":"number","description":"Longitude","example":-122.4233,"format":"double"},"state":{"type":"string","description":"State","example":"CA"}},"description":"Geographical location","example":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"required":["lat","long","city","state"]},"PeriodResponseBody":{"title":"PeriodResponseBody","type":"object","properties":{"endTime":{"type":"string","description":"End time","example":"2020-01-01T00:00:00Z","format":"date-time"},"name":{"type":"string","description":"Period name","example":"Morning"},"startTime":{"type":"string","description":"Start time","example":"2020-01-01T00:00:00Z","format":"date-time"},"summary":{"type":"string","description":"Summary","example":"Clear"},"temperature":{"type":"integer","description":"Temperature","example":70,"format":"int64"},"temperatureUnit":{"type":"string","description":"Temperature unit","example":"F"}},"description":"Weather forecast period","example":{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},"required":["name","startTime","endTime","temperature","temperatureUnit","summary"]},"TestCollectionResponseBody":{"title":"TestCollectionResponseBody","type":"object","properties":{"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"name":{"type":"string","description":"Name of the test collection","example":"Locator Tests"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"},"results":{"type":"array","items":{"$ref":"#/definitions/TestResultResponseBody"},"description":"Test results","example":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}},"description":"Collection of test results for grouping by service","example":{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},"required":["name","duration","pass_count","fail_count"]},"TestResultResponseBody":{"title":"TestResultResponseBody","type":"object","properties":{"duration":{"type":"integer","description":"Duration of the test in ms","example":1234,"format":"int64"},"error":{"type":"string","description":"Error message if the test failed","example":"error getting location for valid ip: %v"},"name":{"type":"string","description":"Name of the test","example":"TestValidIP"},"passed":{"type":"boolean","description":"Status of the test","example":true}},"description":"Test result for a single test","example":{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},"required":["name","passed","duration"]}}}
\ No newline at end of file
+{"swagger":"2.0","info":{"title":"Weather Forecast Service API","description":"The weather forecast service API produces weather forecasts from US-based IPs. It uses IP location to find the appropriate weather station.","version":"1.0.0"},"host":"localhost:80","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/forecast/{ip}":{"get":{"tags":["front"],"summary":"forecast front","description":"Retrieve weather forecast for given IP","operationId":"front#forecast","parameters":[{"name":"ip","in":"path","required":true,"type":"string","format":"ip"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/FrontForecastResponseBody","required":["location","periods"]}},"400":{"description":"Bad Request response.","schema":{"$ref":"#/definitions/FrontForecastNotUsaResponseBody"}}},"schemes":["http"]}},"/tester/all":{"post":{"tags":["front"],"summary":"test_all front","description":"Endpoint for running ALL API Integration Tests for the Weather System, allowing for filtering on included or excluded tests","operationId":"front#test_all","parameters":[{"name":"test_all_request_body","in":"body","required":true,"schema":{"$ref":"#/definitions/FrontTestAllRequestBody"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/FrontTestAllResponseBody","required":["collections","duration","pass_count","fail_count"]}}},"schemes":["http"]}},"/tester/smoke":{"post":{"tags":["front"],"summary":"test_smoke front","description":"Endpoint for running API Integration Tests' Smoke Tests ONLY for the Weather System","operationId":"front#test_smoke","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/FrontTestSmokeResponseBody","required":["collections","duration","pass_count","fail_count"]}}},"schemes":["http"]}}},"definitions":{"FrontForecastNotUsaResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"IP address is not in the US (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"FrontForecastResponseBody":{"title":"FrontForecastResponseBody","type":"object","properties":{"location":{"$ref":"#/definitions/LocationResponseBody"},"periods":{"type":"array","items":{"$ref":"#/definitions/PeriodResponseBody"},"description":"Weather forecast periods","example":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]}},"example":{"location":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"periods":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]},"required":["location","periods"]},"FrontTestAllRequestBody":{"title":"FrontTestAllRequestBody","type":"object","properties":{"exclude":{"type":"array","items":{"type":"string","example":"Eos nihil accusamus reiciendis eligendi amet aut."},"description":"Tests to exclude","example":["Hic aperiam.","Assumenda commodi aut facilis."]},"include":{"type":"array","items":{"type":"string","example":"Corrupti dignissimos id necessitatibus consequatur a laudantium."},"description":"Tests to run","example":["Nihil voluptatum dolorem nam quisquam.","Incidunt ipsa natus."]}},"example":{"exclude":["Asperiores est.","Omnis vel quia ab dolorem qui.","Dicta fuga praesentium.","Est sit quis."],"include":["Aspernatur voluptatem et placeat deserunt.","Temporibus veritatis vel.","Voluptatem ab nulla ad in optio sed.","Tempora tenetur."]}},"FrontTestAllResponseBody":{"title":"FrontTestAllResponseBody","type":"object","properties":{"collections":{"type":"array","items":{"$ref":"#/definitions/TestCollectionResponseBody"},"description":"Test collections","example":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}]},"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"}},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12},"required":["collections","duration","pass_count","fail_count"]},"FrontTestSmokeResponseBody":{"title":"FrontTestSmokeResponseBody","type":"object","properties":{"collections":{"type":"array","items":{"$ref":"#/definitions/TestCollectionResponseBody"},"description":"Test collections","example":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}]},"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"}},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12},"required":["collections","duration","pass_count","fail_count"]},"LocationResponseBody":{"title":"LocationResponseBody","type":"object","properties":{"city":{"type":"string","description":"City","example":"San Francisco"},"lat":{"type":"number","description":"Latitude","example":37.8267,"format":"double"},"long":{"type":"number","description":"Longitude","example":-122.4233,"format":"double"},"state":{"type":"string","description":"State","example":"CA"}},"description":"Geographical location","example":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"required":["lat","long","city","state"]},"PeriodResponseBody":{"title":"PeriodResponseBody","type":"object","properties":{"endTime":{"type":"string","description":"End time","example":"2020-01-01T00:00:00Z","format":"date-time"},"name":{"type":"string","description":"Period name","example":"Morning"},"startTime":{"type":"string","description":"Start time","example":"2020-01-01T00:00:00Z","format":"date-time"},"summary":{"type":"string","description":"Summary","example":"Clear"},"temperature":{"type":"integer","description":"Temperature","example":70,"format":"int64"},"temperatureUnit":{"type":"string","description":"Temperature unit","example":"F"}},"description":"Weather forecast period","example":{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},"required":["name","startTime","endTime","temperature","temperatureUnit","summary"]},"TestCollectionResponseBody":{"title":"TestCollectionResponseBody","type":"object","properties":{"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"name":{"type":"string","description":"Name of the test collection","example":"Locator Tests"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"},"results":{"type":"array","items":{"$ref":"#/definitions/TestResultResponseBody"},"description":"Test results","example":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}},"description":"Collection of test results for grouping by service","example":{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},"required":["name","duration","pass_count","fail_count"]},"TestResultResponseBody":{"title":"TestResultResponseBody","type":"object","properties":{"duration":{"type":"integer","description":"Duration of the test in ms","example":1234,"format":"int64"},"error":{"type":"string","description":"Error message if the test failed","example":"error getting location for valid ip: %v"},"name":{"type":"string","description":"Name of the test","example":"TestValidIP"},"passed":{"type":"boolean","description":"Status of the test","example":true}},"description":"Test result for a single test","example":{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},"required":["name","passed","duration"]}}}
\ No newline at end of file
diff --git a/example/weather/services/front/gen/http/openapi.yaml b/example/weather/services/front/gen/http/openapi.yaml
index 81ed6922..4b85cb24 100644
--- a/example/weather/services/front/gen/http/openapi.yaml
+++ b/example/weather/services/front/gen/http/openapi.yaml
@@ -2,7 +2,7 @@ swagger: "2.0"
info:
title: Weather Forecast Service API
description: The weather forecast service API produces weather forecasts from US-based IPs. It uses IP location to find the appropriate weather station.
- version: ""
+ version: 1.0.0
host: localhost:80
consumes:
- application/json
diff --git a/example/weather/services/front/gen/http/openapi3.json b/example/weather/services/front/gen/http/openapi3.json
index f4e14180..27c175de 100644
--- a/example/weather/services/front/gen/http/openapi3.json
+++ b/example/weather/services/front/gen/http/openapi3.json
@@ -1 +1 @@
-{"openapi":"3.0.3","info":{"title":"Weather Forecast Service API","description":"The weather forecast service API produces weather forecasts from US-based IPs. It uses IP location to find the appropriate weather station.","version":"1.0"},"servers":[{"url":"http://localhost:80","description":"Default server for Weather"}],"paths":{"/forecast/{ip}":{"get":{"tags":["front"],"summary":"forecast front","description":"Retrieve weather forecast for given IP","operationId":"front#forecast","parameters":[{"name":"ip","in":"path","required":true,"schema":{"type":"string","example":"137.67.93.10","format":"ip"},"example":"57.127.156.27"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Forecast"},"example":{"location":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"periods":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]}}}},"400":{"description":"not_usa: IP address is not in the US","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tester/all":{"post":{"tags":["front"],"summary":"test_all front","description":"Endpoint for running ALL API Integration Tests for the Weather System, allowing for filtering on included or excluded tests","operationId":"front#test_all","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAllRequestBody"},"example":{"exclude":["Repudiandae enim molestiae.","Ut id iure."],"include":["Dolor sunt maiores.","Asperiores omnis ducimus ad et mollitia."]}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestResults"},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12}}}}}}},"/tester/smoke":{"post":{"tags":["front"],"summary":"test_smoke front","description":"Endpoint for running API Integration Tests' Smoke Tests ONLY for the Weather System","operationId":"front#test_smoke","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestResults"},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12}}}}}}}},"components":{"schemas":{"Error":{"type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"IP address is not in the US","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"Forecast":{"type":"object","properties":{"location":{"$ref":"#/components/schemas/Location"},"periods":{"type":"array","items":{"$ref":"#/components/schemas/Period"},"description":"Weather forecast periods","example":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]}},"example":{"location":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"periods":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]},"required":["location","periods"]},"Location":{"type":"object","properties":{"city":{"type":"string","description":"City","example":"San Francisco"},"lat":{"type":"number","description":"Latitude","example":37.8267,"format":"double"},"long":{"type":"number","description":"Longitude","example":-122.4233,"format":"double"},"state":{"type":"string","description":"State","example":"CA"}},"description":"Geographical location","example":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"required":["lat","long","city","state"]},"Period":{"type":"object","properties":{"endTime":{"type":"string","description":"End time","example":"2020-01-01T00:00:00Z","format":"date-time"},"name":{"type":"string","description":"Period name","example":"Morning"},"startTime":{"type":"string","description":"Start time","example":"2020-01-01T00:00:00Z","format":"date-time"},"summary":{"type":"string","description":"Summary","example":"Clear"},"temperature":{"type":"integer","description":"Temperature","example":70,"format":"int64"},"temperatureUnit":{"type":"string","description":"Temperature unit","example":"F"}},"description":"Weather forecast period","example":{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},"required":["name","startTime","endTime","temperature","temperatureUnit","summary"]},"TestAllRequestBody":{"type":"object","properties":{"exclude":{"type":"array","items":{"type":"string","example":"Cupiditate voluptas quod minima."},"description":"Tests to exclude","example":["Vitae ut.","Non officiis asperiores rem."]},"include":{"type":"array","items":{"type":"string","example":"Sed officia cum."},"description":"Tests to run","example":["Temporibus ex quo nihil rem.","Et et itaque."]}},"example":{"exclude":["Architecto minima molestiae ad unde.","Porro possimus iste eius minus nesciunt.","Dolores ducimus."],"include":["Architecto aperiam odit.","Iste eveniet."]}},"TestCollection":{"type":"object","properties":{"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"name":{"type":"string","description":"Name of the test collection","example":"Locator Tests"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"},"results":{"type":"array","items":{"$ref":"#/components/schemas/TestResult"},"description":"Test results","example":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}},"description":"Collection of test results for grouping by service","example":{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},"required":["name","duration","pass_count","fail_count"]},"TestResult":{"type":"object","properties":{"duration":{"type":"integer","description":"Duration of the test in ms","example":1234,"format":"int64"},"error":{"type":"string","description":"Error message if the test failed","example":"error getting location for valid ip: %v"},"name":{"type":"string","description":"Name of the test","example":"TestValidIP"},"passed":{"type":"boolean","description":"Status of the test","example":true}},"description":"Test result for a single test","example":{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},"required":["name","passed","duration"]},"TestResults":{"type":"object","properties":{"collections":{"type":"array","items":{"$ref":"#/components/schemas/TestCollection"},"description":"Test collections","example":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}]},"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"}},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12},"required":["collections","duration","pass_count","fail_count"]}}},"tags":[{"name":"front","description":"Public HTTP frontend"}]}
\ No newline at end of file
+{"openapi":"3.0.3","info":{"title":"Weather Forecast Service API","description":"The weather forecast service API produces weather forecasts from US-based IPs. It uses IP location to find the appropriate weather station.","version":"1.0.0"},"servers":[{"url":"http://localhost:80","description":"Default server for Weather"}],"paths":{"/forecast/{ip}":{"get":{"tags":["front"],"summary":"forecast front","description":"Retrieve weather forecast for given IP","operationId":"front#forecast","parameters":[{"name":"ip","in":"path","required":true,"schema":{"type":"string","example":"137.67.93.10","format":"ip"},"example":"57.127.156.27"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Forecast"},"example":{"location":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"periods":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]}}}},"400":{"description":"not_usa: IP address is not in the US","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/tester/all":{"post":{"tags":["front"],"summary":"test_all front","description":"Endpoint for running ALL API Integration Tests for the Weather System, allowing for filtering on included or excluded tests","operationId":"front#test_all","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAllRequestBody"},"example":{"exclude":["Repudiandae enim molestiae.","Ut id iure."],"include":["Dolor sunt maiores.","Asperiores omnis ducimus ad et mollitia."]}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestResults"},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12}}}}}}},"/tester/smoke":{"post":{"tags":["front"],"summary":"test_smoke front","description":"Endpoint for running API Integration Tests' Smoke Tests ONLY for the Weather System","operationId":"front#test_smoke","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestResults"},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12}}}}}}}},"components":{"schemas":{"Error":{"type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"IP address is not in the US","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"Forecast":{"type":"object","properties":{"location":{"$ref":"#/components/schemas/Location"},"periods":{"type":"array","items":{"$ref":"#/components/schemas/Period"},"description":"Weather forecast periods","example":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]}},"example":{"location":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"periods":[{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"}]},"required":["location","periods"]},"Location":{"type":"object","properties":{"city":{"type":"string","description":"City","example":"San Francisco"},"lat":{"type":"number","description":"Latitude","example":37.8267,"format":"double"},"long":{"type":"number","description":"Longitude","example":-122.4233,"format":"double"},"state":{"type":"string","description":"State","example":"CA"}},"description":"Geographical location","example":{"city":"San Francisco","lat":37.8267,"long":-122.4233,"state":"CA"},"required":["lat","long","city","state"]},"Period":{"type":"object","properties":{"endTime":{"type":"string","description":"End time","example":"2020-01-01T00:00:00Z","format":"date-time"},"name":{"type":"string","description":"Period name","example":"Morning"},"startTime":{"type":"string","description":"Start time","example":"2020-01-01T00:00:00Z","format":"date-time"},"summary":{"type":"string","description":"Summary","example":"Clear"},"temperature":{"type":"integer","description":"Temperature","example":70,"format":"int64"},"temperatureUnit":{"type":"string","description":"Temperature unit","example":"F"}},"description":"Weather forecast period","example":{"endTime":"2020-01-01T00:00:00Z","name":"Morning","startTime":"2020-01-01T00:00:00Z","summary":"Clear","temperature":70,"temperatureUnit":"F"},"required":["name","startTime","endTime","temperature","temperatureUnit","summary"]},"TestAllRequestBody":{"type":"object","properties":{"exclude":{"type":"array","items":{"type":"string","example":"Cupiditate voluptas quod minima."},"description":"Tests to exclude","example":["Vitae ut.","Non officiis asperiores rem."]},"include":{"type":"array","items":{"type":"string","example":"Sed officia cum."},"description":"Tests to run","example":["Temporibus ex quo nihil rem.","Et et itaque."]}},"example":{"exclude":["Architecto minima molestiae ad unde.","Porro possimus iste eius minus nesciunt.","Dolores ducimus."],"include":["Architecto aperiam odit.","Iste eveniet."]}},"TestCollection":{"type":"object","properties":{"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"name":{"type":"string","description":"Name of the test collection","example":"Locator Tests"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"},"results":{"type":"array","items":{"$ref":"#/components/schemas/TestResult"},"description":"Test results","example":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}},"description":"Collection of test results for grouping by service","example":{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},"required":["name","duration","pass_count","fail_count"]},"TestResult":{"type":"object","properties":{"duration":{"type":"integer","description":"Duration of the test in ms","example":1234,"format":"int64"},"error":{"type":"string","description":"Error message if the test failed","example":"error getting location for valid ip: %v"},"name":{"type":"string","description":"Name of the test","example":"TestValidIP"},"passed":{"type":"boolean","description":"Status of the test","example":true}},"description":"Test result for a single test","example":{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},"required":["name","passed","duration"]},"TestResults":{"type":"object","properties":{"collections":{"type":"array","items":{"$ref":"#/components/schemas/TestCollection"},"description":"Test collections","example":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}]},"duration":{"type":"integer","description":"Duration of the tests in ms","example":1234,"format":"int64"},"fail_count":{"type":"integer","description":"Number of tests that failed","example":1,"format":"int64"},"pass_count":{"type":"integer","description":"Number of tests that passed","example":12,"format":"int64"}},"example":{"collections":[{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]},{"duration":1234,"fail_count":1,"name":"Locator Tests","pass_count":12,"results":[{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true},{"duration":1234,"error":"error getting location for valid ip: %v","name":"TestValidIP","passed":true}]}],"duration":1234,"fail_count":1,"pass_count":12},"required":["collections","duration","pass_count","fail_count"]}}},"tags":[{"name":"front","description":"Public HTTP frontend"}]}
\ No newline at end of file
diff --git a/example/weather/services/front/gen/http/openapi3.yaml b/example/weather/services/front/gen/http/openapi3.yaml
index ff15b025..858866ad 100644
--- a/example/weather/services/front/gen/http/openapi3.yaml
+++ b/example/weather/services/front/gen/http/openapi3.yaml
@@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: Weather Forecast Service API
description: The weather forecast service API produces weather forecasts from US-based IPs. It uses IP location to find the appropriate weather station.
- version: "1.0"
+ version: 1.0.0
servers:
- url: http://localhost:80
description: Default server for Weather
diff --git a/example/weather/services/locator/cmd/locator/main.go b/example/weather/services/locator/cmd/locator/main.go
index 22095635..82872909 100644
--- a/example/weather/services/locator/cmd/locator/main.go
+++ b/example/weather/services/locator/cmd/locator/main.go
@@ -6,18 +6,22 @@ import (
"fmt"
"net"
"net/http"
+ "net/http/httptrace"
"os"
"os/signal"
"sync"
"syscall"
"time"
+ "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
+ "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
+ "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+ "goa.design/clue/clue"
"goa.design/clue/debug"
"goa.design/clue/health"
"goa.design/clue/log"
- "goa.design/clue/metrics"
- "goa.design/clue/trace"
- goagrpcmiddleware "goa.design/goa/v3/grpc/middleware"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/reflection"
@@ -31,11 +35,10 @@ import (
func main() {
var (
- grpcaddr = flag.String("grpc-addr", ":8082", "gRPC listen address")
- httpaddr = flag.String("http-addr", ":8083", "HTTP listen address (health checks and metrics)")
- agentaddr = flag.String("agent-addr", ":4317", "Grafana agent listen address")
- debugf = flag.Bool("debug", false, "Enable debug logs")
- monitoringEnabled = flag.Bool("monitoring-enabled", true, "monitoring")
+ grpcaddr = flag.String("grpc-addr", ":8082", "gRPC listen address")
+ httpaddr = flag.String("http-addr", ":8083", "HTTP listen address (health checks and metrics)")
+ oteladdr = flag.String("otel-addr", ":4317", "OpenTelemetry collector listen address")
+ debugf = flag.Bool("debug", false, "Enable debug logs")
)
flag.Parse()
@@ -44,60 +47,72 @@ func main() {
if log.IsTerminal() {
format = log.FormatTerminal
}
- ctx := log.Context(context.Background(), log.WithFormat(format), log.WithFunc(trace.Log))
- ctx = log.With(ctx, log.KV{K: "svc", V: genlocator.ServiceName})
+ ctx := log.Context(context.Background(), log.WithFormat(format), log.WithFunc(log.Span))
if *debugf {
ctx = log.Context(ctx, log.WithDebug())
log.Debugf(ctx, "debug logs enabled")
}
- // 2. Setup tracing
- if !*monitoringEnabled {
- var err error
- if ctx, err = trace.Context(ctx, genlocator.ServiceName, trace.WithDisabled()); err != nil {
- log.Error(ctx, err, log.KV{K: "msg", V: "failed to initialize tracing"})
- }
- } else {
- log.Debugf(ctx, "connecting to Grafana agent %s", *agentaddr)
- conn, err := grpc.DialContext(ctx, *agentaddr,
- grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithBlock())
+ // 2. Setup instrumentation
+ spanExporter, err := otlptracegrpc.New(ctx,
+ otlptracegrpc.WithEndpoint(*oteladdr),
+ otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize tracing")
+ }
+ defer func() {
+ err := spanExporter.Shutdown(ctx)
if err != nil {
- log.Errorf(ctx, err, "failed to connect to Grafana agent")
- os.Exit(1)
+ log.Errorf(ctx, err, "failed to shutdown tracing")
}
- log.Debugf(ctx, "connected to Grafana agent %s", *agentaddr)
- ctx, err = trace.Context(ctx, genlocator.ServiceName, trace.WithGRPCExporter(conn))
+ }()
+ metricExporter, err := otlpmetricgrpc.New(ctx,
+ otlpmetricgrpc.WithEndpoint(*oteladdr),
+ otlpmetricgrpc.WithTLSCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize metrics")
+ }
+ defer func() {
+ err := metricExporter.Shutdown(ctx)
if err != nil {
- log.Errorf(ctx, err, "failed to initialize tracing")
- os.Exit(1)
+ log.Errorf(ctx, err, "failed to shutdown metrics")
}
+ }()
+ cfg, err := clue.NewConfig(ctx,
+ genlocator.ServiceName,
+ genlocator.APIVersion,
+ metricExporter,
+ spanExporter,
+ )
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize instrumentation")
}
-
- // 3. Setup metrics
- ctx = metrics.Context(ctx, genlocator.ServiceName)
-
- // 4. Create clients
- c := &http.Client{Transport: log.Client(trace.Client(ctx, http.DefaultTransport))}
- ipc := ipapi.New(c)
-
- // 5. Create service & endpoints
+ clue.ConfigureOpenTelemetry(ctx, cfg)
+
+ // 3. Create clients
+ httpc := &http.Client{
+ Transport: log.Client(
+ otelhttp.NewTransport(
+ http.DefaultTransport,
+ otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {
+ return otelhttptrace.NewClientTrace(ctx)
+ }),
+ ))}
+ ipc := ipapi.New(httpc)
+
+ // 4. Create service & endpoints
svc := locator.New(ipc)
endpoints := genlocator.NewEndpoints(svc)
endpoints.Use(debug.LogPayloads())
endpoints.Use(log.Endpoint)
- // 6. Create transport
+ // 5. Create transport
server := gengrpc.New(endpoints, nil)
grpcsvr := grpc.NewServer(
grpc.ChainUnaryInterceptor(
- goagrpcmiddleware.UnaryRequestID(),
- log.UnaryServerInterceptor(ctx),
- debug.UnaryServerInterceptor(),
- goagrpcmiddleware.UnaryServerLogContext(log.AsGoaMiddlewareLogger),
- trace.UnaryServerInterceptor(ctx),
- metrics.UnaryServerInterceptor(ctx),
- ))
+ log.UnaryServerInterceptor(ctx), // Add logger to request context and log requests.
+ debug.UnaryServerInterceptor()), // Enable debug log level control
+ grpc.StatsHandler(otelgrpc.NewServerHandler())) // Instrument server.
genpb.RegisterLocatorServer(grpcsvr, server)
reflection.Register(grpcsvr)
for svc, info := range grpcsvr.GetServiceInfo() {
@@ -106,17 +121,17 @@ func main() {
}
}
- // 7. Setup health check, metrics and debug endpoints
- check := log.HTTP(ctx)(health.Handler(health.NewChecker(ipc)))
+ // 6. Setup health check and debug endpoints
mux := http.NewServeMux()
debug.MountDebugLogEnabler(mux)
debug.MountPprofHandlers(mux)
+ check := health.Handler(health.NewChecker(ipc))
+ check = log.HTTP(ctx)(check).(http.HandlerFunc) // Log health-check errors
mux.Handle("/healthz", check)
mux.Handle("/livez", check)
- mux.Handle("/metrics", metrics.Handler(ctx))
httpsvr := &http.Server{Addr: *httpaddr, Handler: mux}
- // 8. Start gRPC and HTTP servers
+ // 7. Start gRPC and HTTP servers
errc := make(chan error)
go func() {
c := make(chan os.Signal, 1)
diff --git a/example/weather/services/locator/design/design.go b/example/weather/services/locator/design/design.go
index ad6b1f84..20856a06 100644
--- a/example/weather/services/locator/design/design.go
+++ b/example/weather/services/locator/design/design.go
@@ -7,6 +7,7 @@ import (
var _ = API("IP Location API", func() {
Title("IP Location Service API")
Description("A fully instrumented IP location service API")
+ Version("1.0.0")
})
var _ = Service("locator", func() {
diff --git a/example/weather/services/locator/gen/grpc/cli/ip_location_api/cli.go b/example/weather/services/locator/gen/grpc/cli/ip_location_api/cli.go
index 8763b874..66ffa9af 100644
--- a/example/weather/services/locator/gen/grpc/cli/ip_location_api/cli.go
+++ b/example/weather/services/locator/gen/grpc/cli/ip_location_api/cli.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// IP Location API gRPC client CLI support package
//
diff --git a/example/weather/services/locator/gen/grpc/locator/client/cli.go b/example/weather/services/locator/gen/grpc/locator/client/cli.go
index 5d2f177f..31a902b0 100644
--- a/example/weather/services/locator/gen/grpc/locator/client/cli.go
+++ b/example/weather/services/locator/gen/grpc/locator/client/cli.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator gRPC client CLI support package
//
diff --git a/example/weather/services/locator/gen/grpc/locator/client/client.go b/example/weather/services/locator/gen/grpc/locator/client/client.go
index 6604b57d..f99113f7 100644
--- a/example/weather/services/locator/gen/grpc/locator/client/client.go
+++ b/example/weather/services/locator/gen/grpc/locator/client/client.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator gRPC client
//
diff --git a/example/weather/services/locator/gen/grpc/locator/client/encode_decode.go b/example/weather/services/locator/gen/grpc/locator/client/encode_decode.go
index c2540c12..216a8cb5 100644
--- a/example/weather/services/locator/gen/grpc/locator/client/encode_decode.go
+++ b/example/weather/services/locator/gen/grpc/locator/client/encode_decode.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator gRPC client encoders and decoders
//
diff --git a/example/weather/services/locator/gen/grpc/locator/client/types.go b/example/weather/services/locator/gen/grpc/locator/client/types.go
index 2e747eed..cce8a6f0 100644
--- a/example/weather/services/locator/gen/grpc/locator/client/types.go
+++ b/example/weather/services/locator/gen/grpc/locator/client/types.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator gRPC client types
//
diff --git a/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator.pb.go b/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator.pb.go
index 92fdb0ce..473453b5 100644
--- a/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator.pb.go
+++ b/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator.pb.go
@@ -1,4 +1,4 @@
-// Code generated with goa v3.14.0, DO NOT EDIT.
+// Code generated with goa v3.14.2, DO NOT EDIT.
//
// locator protocol buffer definition
//
@@ -9,7 +9,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
-// protoc v4.25.1
+// protoc v4.25.0
// source: goagen_locator_locator.proto
package locatorpb
diff --git a/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator.proto b/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator.proto
index 3bcc2db9..b7371287 100644
--- a/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator.proto
+++ b/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator.proto
@@ -1,4 +1,4 @@
-// Code generated with goa v3.14.0, DO NOT EDIT.
+// Code generated with goa v3.14.2, DO NOT EDIT.
//
// locator protocol buffer definition
//
diff --git a/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator_grpc.pb.go b/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator_grpc.pb.go
index 0b00a2c7..8980ca5d 100644
--- a/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator_grpc.pb.go
+++ b/example/weather/services/locator/gen/grpc/locator/pb/goagen_locator_locator_grpc.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
-// - protoc v4.25.1
+// - protoc v4.25.0
// source: goagen_locator_locator.proto
package locatorpb
diff --git a/example/weather/services/locator/gen/grpc/locator/server/encode_decode.go b/example/weather/services/locator/gen/grpc/locator/server/encode_decode.go
index 33a08e1d..cdc5afe4 100644
--- a/example/weather/services/locator/gen/grpc/locator/server/encode_decode.go
+++ b/example/weather/services/locator/gen/grpc/locator/server/encode_decode.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator gRPC server encoders and decoders
//
diff --git a/example/weather/services/locator/gen/grpc/locator/server/server.go b/example/weather/services/locator/gen/grpc/locator/server/server.go
index 582e34d1..b99f28ea 100644
--- a/example/weather/services/locator/gen/grpc/locator/server/server.go
+++ b/example/weather/services/locator/gen/grpc/locator/server/server.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator gRPC server
//
diff --git a/example/weather/services/locator/gen/grpc/locator/server/types.go b/example/weather/services/locator/gen/grpc/locator/server/types.go
index 438a092b..1ad3fcb1 100644
--- a/example/weather/services/locator/gen/grpc/locator/server/types.go
+++ b/example/weather/services/locator/gen/grpc/locator/server/types.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator gRPC server types
//
diff --git a/example/weather/services/locator/gen/locator/client.go b/example/weather/services/locator/gen/locator/client.go
index 805c0ae5..9234c426 100644
--- a/example/weather/services/locator/gen/locator/client.go
+++ b/example/weather/services/locator/gen/locator/client.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator client
//
diff --git a/example/weather/services/locator/gen/locator/endpoints.go b/example/weather/services/locator/gen/locator/endpoints.go
index c5ef91ae..458ffe6d 100644
--- a/example/weather/services/locator/gen/locator/endpoints.go
+++ b/example/weather/services/locator/gen/locator/endpoints.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator endpoints
//
diff --git a/example/weather/services/locator/gen/locator/service.go b/example/weather/services/locator/gen/locator/service.go
index 08002896..89075b24 100644
--- a/example/weather/services/locator/gen/locator/service.go
+++ b/example/weather/services/locator/gen/locator/service.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// locator service
//
@@ -23,6 +23,9 @@ type Service interface {
// key.
const ServiceName = "locator"
+// APIVersion is the version of the API as defined in the design.
+const APIVersion = "1.0.0"
+
// MethodNames lists the service method names as defined in the design. These
// are the same values that are set in the endpoint request contexts under the
// MethodKey key.
diff --git a/example/weather/services/tester/README.md b/example/weather/services/tester/README.md
index daf6dea7..243ce0eb 100644
--- a/example/weather/services/tester/README.md
+++ b/example/weather/services/tester/README.md
@@ -18,20 +18,20 @@ The Tester service exposes a gRPC API that allows other services (such as the ex
### TestResults return object
-`TestResults`: This is the main return object for all test methods in the `tester` service and
-it represents the results of the system integration tests. It has four fields: `Collections`
-(an array of `TestCollection`), `Duration` (the total duration of all the tests in milliseconds),
-`PassCount` (the total number of tests that passed), and `FailCount` (the total number of tests
-that failed).
-
-`TestCollection`: This type represents a collection of test results, typically grouped by service.
-It has five fields: `Name` (the name of the test collection), `Results` (an array of `TestResult`),
-`Duration` (the total duration of the tests in the `Collection` in milliseconds), `PassCount` (the
-number of tests that passed), and `FailCount` (the number of tests that failed).
-
-`TestResult`: This type represents the result of a single test. It has four fields: `Name` (the name
-of the test), `Passed` (a boolean indicating whether the test passed), `Error` (an error message if
-the test failed), and `Duration` (the duration of the test in milliseconds).
+`TestResults` serves as the key output for the `tester` service's test methods,
+encapsulating the system integration test outcomes. It details the collective
+test results with fields like `Collections`, an array encompassing various
+`TestCollection` instances, the `Duration` for total test time in milliseconds,
+and counts of both passed (`PassCount`) and failed tests (`FailCount`).
+
+Each `TestCollection` within `TestResults` groups test outcomes, often by
+service. Its structure includes the collection's Name, an array of individual
+`TestResult` items, the total `Duration` for all tests in the collection, and
+counts of successful (`PassCount`) and unsuccessful (`FailCount`) tests.
+
+In `TestResult`, the focus narrows down to individual test performance. This
+includes the specific `Name` of the test, a boolean `Passed` status, a potential
+`Error` message for failures, and the `Duration` each test takes.
### TestSmoke method
diff --git a/example/weather/services/tester/cmd/tester/main.go b/example/weather/services/tester/cmd/tester/main.go
index 113d10e3..a760ee63 100644
--- a/example/weather/services/tester/cmd/tester/main.go
+++ b/example/weather/services/tester/cmd/tester/main.go
@@ -12,12 +12,13 @@ import (
"syscall"
"time"
+ "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
+ "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+ "goa.design/clue/clue"
"goa.design/clue/debug"
"goa.design/clue/health"
"goa.design/clue/log"
- "goa.design/clue/metrics"
- "goa.design/clue/trace"
- goagrpcmiddleware "goa.design/goa/v3/grpc/middleware"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/reflection"
@@ -34,13 +35,12 @@ func main() {
var (
grpcaddr = flag.String("grpc-addr", ":8090", "gRPC listen address")
httpaddr = flag.String("http-addr", ":8091", "HTTP listen address (health checks and metrics)")
- agentaddr = flag.String("agent-addr", ":4317", "Grafana agent listen address")
+ oteladdr = flag.String("otel-addr", ":4317", "OpenTelemetry collector listen address")
forecasterAddr = flag.String("forecaster-addr", ":8080", "Forecaster service address")
forecasterHealthAddr = flag.String("forecaster-health-addr", ":8081", "Forecaster service health-check address")
locatorAddr = flag.String("locator-addr", ":8082", "Locator service address")
locatorHealthAddr = flag.String("locator-health-addr", ":8083", "Locator service health-check address")
debugf = flag.Bool("debug", false, "Enable debug logs")
- monitoringEnabled = flag.Bool("monitoring-enabled", true, "monitoring")
)
flag.Parse()
@@ -49,78 +49,80 @@ func main() {
if log.IsTerminal() {
format = log.FormatTerminal
}
- ctx := log.Context(context.Background(), log.WithFormat(format), log.WithFunc(trace.Log))
- ctx = log.With(ctx, log.KV{K: "svc", V: gentester.ServiceName})
+ ctx := log.Context(context.Background(), log.WithFormat(format), log.WithFunc(log.Span))
if *debugf {
ctx = log.Context(ctx, log.WithDebug())
log.Debugf(ctx, "debug logs enabled")
}
- // 2. Setup tracing
- if !*monitoringEnabled {
- var err error
- if ctx, err = trace.Context(ctx, gentester.ServiceName, trace.WithDisabled()); err != nil {
- log.Error(ctx, err, log.KV{K: "msg", V: "failed to initialize tracing"})
- }
- } else {
- log.Debugf(ctx, "connecting to Grafana agent %s", *agentaddr)
- conn, err := grpc.DialContext(ctx, *agentaddr,
- grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithBlock())
+ // 2. Setup isntrumentation
+ spanExporter, err := otlptracegrpc.New(ctx,
+ otlptracegrpc.WithEndpoint(*oteladdr),
+ otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize tracing")
+ }
+ defer func() {
+ err := spanExporter.Shutdown(ctx)
if err != nil {
- log.Errorf(ctx, err, "failed to connect to Grafana agent")
- os.Exit(1)
+ log.Errorf(ctx, err, "failed to shutdown tracing")
}
- log.Debugf(ctx, "connected to Grafana agent %s", *agentaddr)
- ctx, err = trace.Context(ctx, gentester.ServiceName, trace.WithGRPCExporter(conn))
+ }()
+ metricExporter, err := otlpmetricgrpc.New(ctx,
+ otlpmetricgrpc.WithEndpoint(*oteladdr),
+ otlpmetricgrpc.WithTLSCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize metrics")
+ }
+ defer func() {
+ err := metricExporter.Shutdown(ctx)
if err != nil {
- log.Errorf(ctx, err, "failed to initialize tracing")
- os.Exit(1)
+ log.Errorf(ctx, err, "failed to shutdown metrics")
}
+ }()
+ cfg, err := clue.NewConfig(ctx,
+ gentester.ServiceName,
+ gentester.APIVersion,
+ metricExporter,
+ spanExporter,
+ )
+ if err != nil {
+ log.Fatalf(ctx, err, "failed to initialize instrumentation")
}
+ clue.ConfigureOpenTelemetry(ctx, cfg)
- // 3. Setup metrics
- ctx = metrics.Context(ctx, gentester.ServiceName)
-
- // 4. Create clients
- lcc, err := grpc.DialContext(ctx, *locatorAddr,
+ // 3. Create clients
+ lcc, err := grpc.DialContext(ctx,
+ *locatorAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithChainUnaryInterceptor(
- trace.UnaryClientInterceptor(ctx),
- log.UnaryClientInterceptor()))
+ grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()),
+ grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
if err != nil {
- log.Errorf(ctx, err, "failed to connect to locator")
- os.Exit(1)
+ log.Fatalf(ctx, err, "failed to connect to locator")
}
lc := locator.New(lcc)
fcc, err := grpc.DialContext(ctx, *forecasterAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithChainUnaryInterceptor(
- trace.UnaryClientInterceptor(ctx),
- log.UnaryClientInterceptor()))
+ grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()),
+ grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
if err != nil {
- log.Errorf(ctx, err, "failed to connect to forecast")
- os.Exit(1)
+ log.Fatalf(ctx, err, "failed to connect to forecast")
}
fc := forecaster.New(fcc)
- // 5. Create service & endpoints
+ // 4. Create service & endpoints
svc := tester.New(lc, fc)
endpoints := gentester.NewEndpoints(svc)
endpoints.Use(debug.LogPayloads())
endpoints.Use(log.Endpoint)
- // 6. Create transport
+ // 5. Create transport
server := gengrpc.New(endpoints, nil)
grpcsvr := grpc.NewServer(
grpc.ChainUnaryInterceptor(
- goagrpcmiddleware.UnaryRequestID(),
log.UnaryServerInterceptor(ctx),
- debug.UnaryServerInterceptor(),
- goagrpcmiddleware.UnaryServerLogContext(log.AsGoaMiddlewareLogger),
- trace.UnaryServerInterceptor(ctx),
- metrics.UnaryServerInterceptor(ctx),
- ))
+ debug.UnaryServerInterceptor()),
+ grpc.StatsHandler(otelgrpc.NewServerHandler()))
genpb.RegisterTesterServer(grpcsvr, server)
reflection.Register(grpcsvr)
for svc, info := range grpcsvr.GetServiceInfo() {
@@ -129,19 +131,18 @@ func main() {
}
}
- // 7. Setup health check, metrics and debug endpoints
- check := health.Handler(health.NewChecker(
- health.NewPinger("locator", *locatorHealthAddr),
- health.NewPinger("forecaster", *forecasterHealthAddr)))
+ // 6. Setup health check, metrics and debug endpoints
mux := http.NewServeMux()
debug.MountDebugLogEnabler(mux)
debug.MountPprofHandlers(mux)
+ check := health.Handler(health.NewChecker(
+ health.NewPinger("locator", *locatorHealthAddr),
+ health.NewPinger("forecaster", *forecasterHealthAddr)))
mux.Handle("/healthz", check)
mux.Handle("/livez", check)
- mux.Handle("/metrics", metrics.Handler(ctx))
httpsvr := &http.Server{Addr: *httpaddr, Handler: mux}
- // 8. Start gRPC and HTTP servers
+ // 7. Start gRPC and HTTP servers
errc := make(chan error)
go func() {
c := make(chan os.Signal, 1)
diff --git a/example/weather/services/tester/gen/grpc/cli/tester_service_api/cli.go b/example/weather/services/tester/gen/grpc/cli/tester_service_api/cli.go
index 4bad40b3..c377a24e 100644
--- a/example/weather/services/tester/gen/grpc/cli/tester_service_api/cli.go
+++ b/example/weather/services/tester/gen/grpc/cli/tester_service_api/cli.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// Tester Service API gRPC client CLI support package
//
diff --git a/example/weather/services/tester/gen/grpc/tester/client/cli.go b/example/weather/services/tester/gen/grpc/tester/client/cli.go
index 639528d0..541100e3 100644
--- a/example/weather/services/tester/gen/grpc/tester/client/cli.go
+++ b/example/weather/services/tester/gen/grpc/tester/client/cli.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester gRPC client CLI support package
//
diff --git a/example/weather/services/tester/gen/grpc/tester/client/client.go b/example/weather/services/tester/gen/grpc/tester/client/client.go
index 195e0922..1966b68e 100644
--- a/example/weather/services/tester/gen/grpc/tester/client/client.go
+++ b/example/weather/services/tester/gen/grpc/tester/client/client.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester gRPC client
//
diff --git a/example/weather/services/tester/gen/grpc/tester/client/encode_decode.go b/example/weather/services/tester/gen/grpc/tester/client/encode_decode.go
index 62cac956..2907efed 100644
--- a/example/weather/services/tester/gen/grpc/tester/client/encode_decode.go
+++ b/example/weather/services/tester/gen/grpc/tester/client/encode_decode.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester gRPC client encoders and decoders
//
diff --git a/example/weather/services/tester/gen/grpc/tester/client/types.go b/example/weather/services/tester/gen/grpc/tester/client/types.go
index 4b409045..96552f15 100644
--- a/example/weather/services/tester/gen/grpc/tester/client/types.go
+++ b/example/weather/services/tester/gen/grpc/tester/client/types.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester gRPC client types
//
diff --git a/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester.pb.go b/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester.pb.go
index 16273fed..6c1c5a55 100644
--- a/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester.pb.go
+++ b/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester.pb.go
@@ -1,4 +1,4 @@
-// Code generated with goa v3.14.0, DO NOT EDIT.
+// Code generated with goa v3.14.2, DO NOT EDIT.
//
// tester protocol buffer definition
//
@@ -9,7 +9,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
-// protoc v4.25.1
+// protoc v4.25.0
// source: goagen_tester_tester.proto
package weather_testerpb
diff --git a/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester.proto b/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester.proto
index 29f1bb52..56bf1064 100644
--- a/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester.proto
+++ b/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester.proto
@@ -1,4 +1,4 @@
-// Code generated with goa v3.14.0, DO NOT EDIT.
+// Code generated with goa v3.14.2, DO NOT EDIT.
//
// tester protocol buffer definition
//
diff --git a/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester_grpc.pb.go b/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester_grpc.pb.go
index 41554e77..9303cc6a 100644
--- a/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester_grpc.pb.go
+++ b/example/weather/services/tester/gen/grpc/tester/pb/goagen_tester_tester_grpc.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
-// - protoc v4.25.1
+// - protoc v4.25.0
// source: goagen_tester_tester.proto
package weather_testerpb
diff --git a/example/weather/services/tester/gen/grpc/tester/server/encode_decode.go b/example/weather/services/tester/gen/grpc/tester/server/encode_decode.go
index 258e3976..47ccf41e 100644
--- a/example/weather/services/tester/gen/grpc/tester/server/encode_decode.go
+++ b/example/weather/services/tester/gen/grpc/tester/server/encode_decode.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester gRPC server encoders and decoders
//
diff --git a/example/weather/services/tester/gen/grpc/tester/server/server.go b/example/weather/services/tester/gen/grpc/tester/server/server.go
index 6dd4c5cf..9f0a89b2 100644
--- a/example/weather/services/tester/gen/grpc/tester/server/server.go
+++ b/example/weather/services/tester/gen/grpc/tester/server/server.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester gRPC server
//
diff --git a/example/weather/services/tester/gen/grpc/tester/server/types.go b/example/weather/services/tester/gen/grpc/tester/server/types.go
index dd4b8b9d..5eee7564 100644
--- a/example/weather/services/tester/gen/grpc/tester/server/types.go
+++ b/example/weather/services/tester/gen/grpc/tester/server/types.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester gRPC server types
//
diff --git a/example/weather/services/tester/gen/tester/client.go b/example/weather/services/tester/gen/tester/client.go
index 47c861b3..f87f064b 100644
--- a/example/weather/services/tester/gen/tester/client.go
+++ b/example/weather/services/tester/gen/tester/client.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester client
//
diff --git a/example/weather/services/tester/gen/tester/endpoints.go b/example/weather/services/tester/gen/tester/endpoints.go
index 5da528ce..4bc0bcb7 100644
--- a/example/weather/services/tester/gen/tester/endpoints.go
+++ b/example/weather/services/tester/gen/tester/endpoints.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester endpoints
//
diff --git a/example/weather/services/tester/gen/tester/service.go b/example/weather/services/tester/gen/tester/service.go
index 7e1de8e0..64020e95 100644
--- a/example/weather/services/tester/gen/tester/service.go
+++ b/example/weather/services/tester/gen/tester/service.go
@@ -1,4 +1,4 @@
-// Code generated by goa v3.14.0, DO NOT EDIT.
+// Code generated by goa v3.14.2, DO NOT EDIT.
//
// tester service
//
@@ -32,6 +32,9 @@ type Service interface {
// key.
const ServiceName = "tester"
+// APIVersion is the version of the API as defined in the design.
+const APIVersion = "0.0.1"
+
// MethodNames lists the service method names as defined in the design. These
// are the same values that are set in the endpoint request contexts under the
// MethodKey key.
diff --git a/example/weather/services/tester/run_tests.go b/example/weather/services/tester/run_tests.go
index 22a1cc7c..eebb07a5 100644
--- a/example/weather/services/tester/run_tests.go
+++ b/example/weather/services/tester/run_tests.go
@@ -12,9 +12,10 @@ import (
"sync"
"time"
- gentester "goa.design/clue/example/weather/services/tester/gen/tester"
"goa.design/clue/log"
"golang.org/x/exp/slices"
+
+ gentester "goa.design/clue/example/weather/services/tester/gen/tester"
)
// Ends a test by calculating duration and appending the results to the test collection
@@ -40,7 +41,7 @@ func getStackTrace(wg *sync.WaitGroup, m *sync.Mutex) string {
outC := make(chan string)
go func() {
var buf bytes.Buffer
- io.Copy(&buf, f)
+ io.Copy(&buf, f) // nolint: errcheck
outC <- buf.String()
}()
diff --git a/example/weather/services/tester/testing.go b/example/weather/services/tester/testing.go
index c4595935..a691a953 100644
--- a/example/weather/services/tester/testing.go
+++ b/example/weather/services/tester/testing.go
@@ -3,9 +3,10 @@ package tester
import (
"context"
+ "goa.design/clue/log"
+
"goa.design/clue/example/weather/services/tester/clients/forecaster"
"goa.design/clue/example/weather/services/tester/clients/locator"
- "goa.design/clue/log"
)
type (
diff --git a/go.mod b/go.mod
index de7c432e..21bccb97 100644
--- a/go.mod
+++ b/go.mod
@@ -7,18 +7,17 @@ toolchain go1.21.3
require (
github.com/aws/smithy-go v1.19.0
github.com/go-logfmt/logfmt v0.6.0
- github.com/prometheus/client_golang v1.18.0
- github.com/prometheus/client_model v0.5.0
+ github.com/go-logr/logr v1.4.1
github.com/stretchr/testify v1.8.4
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1
go.opentelemetry.io/otel v1.21.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
+ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0
+ go.opentelemetry.io/otel/metric v1.21.0
go.opentelemetry.io/otel/sdk v1.21.0
+ go.opentelemetry.io/otel/sdk/metric v1.21.0
go.opentelemetry.io/otel/trace v1.21.0
- goa.design/goa/v3 v3.14.1
- golang.org/x/term v0.15.0
+ goa.design/goa/v3 v3.14.2
+ golang.org/x/term v0.16.0
golang.org/x/tools v0.16.1
google.golang.org/grpc v1.60.1
google.golang.org/protobuf v1.32.0
@@ -26,33 +25,22 @@ require (
require (
github.com/AnatolyRugalev/goregen v0.1.0 // indirect
- github.com/beorn7/perks v1.0.1 // indirect
- github.com/cenkalti/backoff/v4 v4.2.1 // indirect
- github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect
- github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-chi/chi/v5 v5.0.11 // indirect
- github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect
- github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/common v0.45.0 // indirect
- github.com/prometheus/procfs v0.12.0 // indirect
+ github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
- go.opentelemetry.io/otel/metric v1.21.0 // indirect
- go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
- golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 3cc9e489..2e79ff2e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,28 +1,13 @@
-cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
-cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
-cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
-cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
github.com/AnatolyRugalev/goregen v0.1.0 h1:xrdXkLaskMnbxW0x4FWNj2yoednv0X2bcTBWpuJGYfE=
github.com/AnatolyRugalev/goregen v0.1.0/go.mod h1:sVlY1tjcirqLBRZnCcIq1+7/Lwmqz5g7IK8AStjOVzI=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
-github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
-github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
-github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI=
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI=
-github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
-github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
@@ -42,8 +27,6 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -55,18 +38,10 @@ github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqC
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s=
github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b h1:3E44bLeN8uKYdfQqVQycPnaVviZdBLbizFhU49mtbe4=
github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b/go.mod h1:Bj8LjjP0ReT1eKt5QlKjwgi5AFm5mI6O1A2G4ChI0Ag=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
-github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
-github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
-github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
-github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
@@ -75,55 +50,39 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgYCza3PXRUGEyCB++y1sAqm6guWFesk=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
+go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
-go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
-go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
-go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
-go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-goa.design/goa/v3 v3.14.1 h1:wNvKwCXeEJWmosd2MXaZHjJGm+wtEPCZf5jNXCx3oJo=
-goa.design/goa/v3 v3.14.1/go.mod h1:MhHWTSB7X6qVuNvjDTNtr/YQyYi9x1I4zfPtXnCdHtQ=
+goa.design/goa/v3 v3.14.2 h1:V8skSFWRUTddqZnRy65Mdx0SpGmYe9aQhgCQppyvCKA=
+goa.design/goa/v3 v3.14.2/go.mod h1:sq7F2y/2/eK/XCc8Ff7Was3u42fRmQXp1pTm8FfnGgo=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
-golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
-golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
-golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
-google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
-google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic=
-google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o=
-google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
@@ -131,9 +90,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/go.work.sum b/go.work.sum
index b52d0fa6..446a792f 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -5,6 +5,7 @@ cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5x
cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk=
cloud.google.com/go v0.110.9/go.mod h1:rpxevX/0Lqvlbc88b7Sc1SPNdyK1riNBTUU6JXhYNpM=
cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
+cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
cloud.google.com/go/accessapproval v1.5.0 h1:/nTivgnV/n1CaAeo+ekGexTYUsKEU9jUVkoY5359+3Q=
cloud.google.com/go/accessapproval v1.7.1/go.mod h1:JYczztsHRMK7NTXb6Xw+dwbs/WnOJxbo/2mTI+Kgg68=
cloud.google.com/go/accessapproval v1.7.3/go.mod h1:4l8+pwIxGTNqSf4T3ds8nLO94NQf0W/KnMNuQ9PbnP8=
@@ -17,6 +18,7 @@ cloud.google.com/go/aiplatform v1.27.0 h1:DBi3Jk9XjCJ4pkkLM4NqKgj3ozUL1wq4l+d3/j
cloud.google.com/go/aiplatform v1.48.0/go.mod h1:Iu2Q7sC7QGhXUeOhAj/oCK9a+ULz1O4AotZiqjQ8MYA=
cloud.google.com/go/aiplatform v1.51.2/go.mod h1:hCqVYB3mY45w99TmetEoe8eCQEwZEp9WHxeZdcv9phw=
cloud.google.com/go/aiplatform v1.54.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU=
+cloud.google.com/go/aiplatform v1.57.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU=
cloud.google.com/go/analytics v0.12.0 h1:NKw6PpQi6V1O+KsjuTd+bhip9d0REYu4NevC45vtGp8=
cloud.google.com/go/analytics v0.21.3/go.mod h1:U8dcUtmDmjrmUTnnnRnI4m6zKn/yaA5N9RlEkYFHpQo=
cloud.google.com/go/analytics v0.21.5/go.mod h1:BQtOBHWTlJ96axpPPnw5CvGJ6i3Ve/qX2fTxR8qWyr8=
@@ -64,6 +66,7 @@ cloud.google.com/go/batch v0.4.0 h1:1jvEBY55OH4Sd2FxEXQfxGExFWov1A/IaRe+Z5Z71Fw=
cloud.google.com/go/batch v1.3.1/go.mod h1:VguXeQKXIYaeeIYbuozUmBR13AfL4SJP7IltNPS+A4A=
cloud.google.com/go/batch v1.6.1/go.mod h1:urdpD13zPe6YOK+6iZs/8/x2VBRofvblLpx0t57vM98=
cloud.google.com/go/batch v1.6.3/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU=
+cloud.google.com/go/batch v1.7.0/go.mod h1:J64gD4vsNSA2O5TtDB5AAux3nJ9iV8U3ilg3JDBYejU=
cloud.google.com/go/beyondcorp v0.3.0 h1:w+4kThysgl0JiKshi2MKDCg2NZgOyqOI0wq2eBZyrzA=
cloud.google.com/go/beyondcorp v1.0.0/go.mod h1:YhxDWw946SCbmcWo3fAhw3V4XZMSpQ/VYfcKGAEU8/4=
cloud.google.com/go/beyondcorp v1.0.2/go.mod h1:m8cpG7caD+5su+1eZr+TSvF6r21NdLJk4f9u4SP2Ntc=
@@ -76,10 +79,12 @@ cloud.google.com/go/billing v1.7.0 h1:Xkii76HWELHwBtkQVZvqmSo9GTr0O+tIbRNnMcGdlg
cloud.google.com/go/billing v1.16.0/go.mod h1:y8vx09JSSJG02k5QxbycNRrN7FGZB6F3CAcgum7jvGA=
cloud.google.com/go/billing v1.17.3/go.mod h1:z83AkoZ7mZwBGT3yTnt6rSGI1OOsHSIi6a5M3mJ8NaU=
cloud.google.com/go/billing v1.17.4/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk=
+cloud.google.com/go/billing v1.18.0/go.mod h1:5DOYQStCxquGprqfuid/7haD7th74kyMBHkjO/OvDtk=
cloud.google.com/go/binaryauthorization v1.4.0 h1:pL70vXWn9TitQYXBWTK2abHl2JHLwkFRjYw6VflRqEA=
cloud.google.com/go/binaryauthorization v1.6.1/go.mod h1:TKt4pa8xhowwffiBmbrbcxijJRZED4zrqnwZ1lKH51U=
cloud.google.com/go/binaryauthorization v1.7.2/go.mod h1:kFK5fQtxEp97m92ziy+hbu+uKocka1qRRL8MVJIgjv0=
cloud.google.com/go/binaryauthorization v1.7.3/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU=
+cloud.google.com/go/binaryauthorization v1.8.0/go.mod h1:VQ/nUGRKhrStlGr+8GMS8f6/vznYLkdK5vaKfdCIpvU=
cloud.google.com/go/certificatemanager v1.4.0 h1:tzbR4UHBbgsewMWUD93JHi8EBi/gHBoSAcY1/sThFGk=
cloud.google.com/go/certificatemanager v1.7.1/go.mod h1:iW8J3nG6SaRYImIa+wXQ0g8IgoofDFRp5UMzaNk1UqI=
cloud.google.com/go/certificatemanager v1.7.3/go.mod h1:T/sZYuC30PTag0TLo28VedIRIj1KPGcOQzjWAptHa00=
@@ -110,10 +115,12 @@ cloud.google.com/go/contactcenterinsights v1.4.0 h1:tTQLI/ZvguUf9Hv+36BkG2+/PeC8
cloud.google.com/go/contactcenterinsights v1.10.0/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM=
cloud.google.com/go/contactcenterinsights v1.11.2/go.mod h1:A9PIR5ov5cRcd28KlDbmmXE8Aay+Gccer2h4wzkYFso=
cloud.google.com/go/contactcenterinsights v1.12.0/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis=
+cloud.google.com/go/contactcenterinsights v1.12.1/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis=
cloud.google.com/go/container v1.7.0 h1:nbEK/59GyDRKKlo1SqpohY1TK8LmJ2XNcvS9Gyom2A0=
cloud.google.com/go/container v1.24.0/go.mod h1:lTNExE2R7f+DLbAN+rJiKTisauFCaoDq6NURZ83eVH4=
cloud.google.com/go/container v1.26.2/go.mod h1:YlO84xCt5xupVbLaMY4s3XNE79MUJ+49VmkInr6HvF4=
cloud.google.com/go/container v1.28.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4=
+cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4=
cloud.google.com/go/containeranalysis v0.6.0 h1:2824iym832ljKdVpCBnpqm5K94YT/uHTVhNF+dRTXPI=
cloud.google.com/go/containeranalysis v0.10.1/go.mod h1:Ya2jiILITMY68ZLPaogjmOMNkwsDrWBSTyBubGXO7j0=
cloud.google.com/go/containeranalysis v0.11.2/go.mod h1:xibioGBC1MD2j4reTyV1xY1/MvKaz+fyM9ENWhmIeP8=
@@ -142,6 +149,7 @@ cloud.google.com/go/dataplex v1.4.0 h1:cNxeA2DiWliQGi21kPRqnVeQ5xFhNoEjPRt1400Pm
cloud.google.com/go/dataplex v1.9.0/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE=
cloud.google.com/go/dataplex v1.10.2/go.mod h1:xdC8URdTrCrZMW6keY779ZT1cTOfV8KEPNsw+LTRT1Y=
cloud.google.com/go/dataplex v1.11.2/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c=
+cloud.google.com/go/dataplex v1.13.0/go.mod h1:mHJYQQ2VEJHsyoC0OdNyy988DvEbPhqFs5OOLffLX0c=
cloud.google.com/go/dataproc v1.8.0 h1:gVOqNmElfa6n/ccG/QDlfurMWwrK3ezvy2b2eDoCmS0=
cloud.google.com/go/dataproc/v2 v2.0.1/go.mod h1:7Ez3KRHdFGcfY7GcevBbvozX+zyWGcwLJvvAMwCaoZ4=
cloud.google.com/go/dataproc/v2 v2.2.2/go.mod h1:aocQywVmQVF4i8CL740rNI/ZRpsaaC1Wh2++BJ7HEJ4=
@@ -161,10 +169,12 @@ cloud.google.com/go/deploy v1.5.0 h1:kI6dxt8Ml0is/x7YZjLveTvR7YPzXAUD/8wQZ2nH5zA
cloud.google.com/go/deploy v1.13.0/go.mod h1:tKuSUV5pXbn67KiubiUNUejqLs4f5cxxiCNCeyl0F2g=
cloud.google.com/go/deploy v1.14.1/go.mod h1:N8S0b+aIHSEeSr5ORVoC0+/mOPUysVt8ae4QkZYolAw=
cloud.google.com/go/deploy v1.15.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g=
+cloud.google.com/go/deploy v1.16.0/go.mod h1:e5XOUI5D+YGldyLNZ21wbp9S8otJbBE4i88PtO9x/2g=
cloud.google.com/go/dialogflow v1.19.0 h1:HYHVOkoxQ9bSfNIelSZYNAtUi4CeSrCnROyOsbOqPq8=
cloud.google.com/go/dialogflow v1.40.0/go.mod h1:L7jnH+JL2mtmdChzAIcXQHXMvQkE3U4hTaNltEuxXn4=
cloud.google.com/go/dialogflow v1.44.2/go.mod h1:QzFYndeJhpVPElnFkUXxdlptx0wPnBWLCBT9BvtC3/c=
cloud.google.com/go/dialogflow v1.44.3/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ=
+cloud.google.com/go/dialogflow v1.47.0/go.mod h1:mHly4vU7cPXVweuB5R0zsYKPMzy240aQdAu06SqBbAQ=
cloud.google.com/go/dlp v1.7.0 h1:9I4BYeJSVKoSKgjr70fLdRDumqcUeVmHV4fd5f9LR6Y=
cloud.google.com/go/dlp v1.10.1/go.mod h1:IM8BWz1iJd8njcNcG0+Kyd9OPnqnRNkDV8j42VT5KOI=
cloud.google.com/go/dlp v1.10.3/go.mod h1:iUaTc/ln8I+QT6Ai5vmuwfw8fqTk2kaz0FvCwhLCom0=
@@ -173,6 +183,7 @@ cloud.google.com/go/documentai v1.10.0 h1:jfq09Fdjtnpnmt/MLyf6A3DM3ynb8B2na0K+vS
cloud.google.com/go/documentai v1.22.0/go.mod h1:yJkInoMcK0qNAEdRnqY/D5asy73tnPe88I1YTZT+a8E=
cloud.google.com/go/documentai v1.23.4/go.mod h1:4MYAaEMnADPN1LPN5xboDR5QVB6AgsaxgFdJhitlE2Y=
cloud.google.com/go/documentai v1.23.5/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g=
+cloud.google.com/go/documentai v1.23.6/go.mod h1:ghzBsyVTiVdkfKaUCum/9bGBEyBjDO4GfooEcYKhN+g=
cloud.google.com/go/domains v0.7.0 h1:pu3JIgC1rswIqi5romW0JgNO6CTUydLYX8zyjiAvO1c=
cloud.google.com/go/domains v0.9.1/go.mod h1:aOp1c0MbejQQ2Pjf1iJvnVyT+z6R6s8pX66KaCSDYfE=
cloud.google.com/go/domains v0.9.3/go.mod h1:29k66YNDLDY9LCFKpGFeh6Nj9r62ZKm5EsUJxAl84KU=
@@ -226,6 +237,7 @@ cloud.google.com/go/gsuiteaddons v1.6.4/go.mod h1:rxtstw7Fx22uLOXBpsvb9DUbC+fiXs
cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk=
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU=
+cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE=
cloud.google.com/go/iam v1.1.4/go.mod h1:l/rg8l1AaA+VFMho/HYx2Vv6xinPSLMF8qfhRPIZ0L8=
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
cloud.google.com/go/iap v1.5.0 h1:BGEXovwejOCt1zDk8hXq0bOhhRu9haXKWXXXp2B4wBM=
@@ -269,6 +281,7 @@ cloud.google.com/go/maps v0.1.0 h1:kLReRbclTgJefw2fcCbdLPLhPj0U6UUWN10ldG8sdOU=
cloud.google.com/go/maps v1.4.0/go.mod h1:6mWTUv+WhnOwAgjVsSW2QPPECmW+s3PcRyOa9vgG/5s=
cloud.google.com/go/maps v1.5.1/go.mod h1:NPMZw1LJwQZYCfz4y+EIw+SI+24A4bpdFJqdKVr0lt4=
cloud.google.com/go/maps v1.6.1/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18=
+cloud.google.com/go/maps v1.6.2/go.mod h1:4+buOHhYXFBp58Zj/K+Lc1rCmJssxxF4pJ5CJnhdz18=
cloud.google.com/go/mediatranslation v0.6.0 h1:qAJzpxmEX+SeND10Y/4868L5wfZpo4Y3BIEnIieP4dk=
cloud.google.com/go/mediatranslation v0.8.1/go.mod h1:L/7hBdEYbYHQJhX2sldtTO5SZZ1C1vkapubj0T2aGig=
cloud.google.com/go/mediatranslation v0.8.3/go.mod h1:F9OnXTy336rteOEywtY7FOqCk+J43o2RF638hkOQl4Y=
@@ -341,6 +354,7 @@ cloud.google.com/go/recaptchaenterprise/v2 v2.5.0 h1:UqzFfb/WvhwXGDF1eQtdHLrmni+
cloud.google.com/go/recaptchaenterprise/v2 v2.7.2/go.mod h1:kR0KjsJS7Jt1YSyWFkseQ756D45kaYNTlDPPaRAvDBU=
cloud.google.com/go/recaptchaenterprise/v2 v2.8.2/go.mod h1:kpaDBOpkwD4G0GVMzG1W6Doy1tFFC97XAV3xy+Rd/pw=
cloud.google.com/go/recaptchaenterprise/v2 v2.8.4/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w=
+cloud.google.com/go/recaptchaenterprise/v2 v2.9.0/go.mod h1:Dak54rw6lC2gBY8FBznpOCAR58wKf+R+ZSJRoeJok4w=
cloud.google.com/go/recommendationengine v0.6.0 h1:6w+WxPf2LmUEqX0YyvfCoYb8aBYOcbIV25Vg6R0FLGw=
cloud.google.com/go/recommendationengine v0.8.1/go.mod h1:MrZihWwtFYWDzE6Hz5nKcNz3gLizXVIDI/o3G1DLcrE=
cloud.google.com/go/recommendationengine v0.8.3/go.mod h1:m3b0RZV02BnODE9FeSvGv1qibFo8g0OnmB/RMwYy4V8=
@@ -385,6 +399,7 @@ cloud.google.com/go/securitycenter v1.16.0 h1:QTVtk/Reqnx2bVIZtJKm1+mpfmwRwymmNv
cloud.google.com/go/securitycenter v1.23.0/go.mod h1:8pwQ4n+Y9WCWM278R8W3nF65QtY172h4S8aXyI9/hsQ=
cloud.google.com/go/securitycenter v1.24.1/go.mod h1:3h9IdjjHhVMXdQnmqzVnM7b0wMn/1O/U20eWVpMpZjI=
cloud.google.com/go/securitycenter v1.24.2/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM=
+cloud.google.com/go/securitycenter v1.24.3/go.mod h1:l1XejOngggzqwr4Fa2Cn+iWZGf+aBLTXtB/vXjy5vXM=
cloud.google.com/go/servicecontrol v1.5.0 h1:ImIzbOu6y4jL6ob65I++QzvqgFaoAKgHOG+RU9/c4y8=
cloud.google.com/go/servicedirectory v1.7.0 h1:f7M8IMcVzO3T425AqlZbP3yLzeipsBHtRza8vVFYMhQ=
cloud.google.com/go/servicedirectory v1.11.0/go.mod h1:Xv0YVH8s4pVOwfM/1eMTl0XJ6bzIOSLDt8f8eLaGOxQ=
@@ -400,6 +415,7 @@ cloud.google.com/go/spanner v1.41.0 h1:NvdTpRwf7DTegbfFdPjAWyD7bOVu0VeMqcvR9aCQC
cloud.google.com/go/spanner v1.47.0/go.mod h1:IXsJwVW2j4UKs0eYDqodab6HgGuA1bViSqW4uH9lfUI=
cloud.google.com/go/spanner v1.51.0/go.mod h1:c5KNo5LQ1X5tJwma9rSQZsXNBDNvj4/n8BVc3LNahq0=
cloud.google.com/go/spanner v1.53.0/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws=
+cloud.google.com/go/spanner v1.53.1/go.mod h1:liG4iCeLqm5L3fFLU5whFITqP0e0orsAW1uUSrd4rws=
cloud.google.com/go/speech v1.9.0 h1:yK0ocnFH4Wsf0cMdUyndJQ/hPv02oTJOxzi6AgpBy4s=
cloud.google.com/go/speech v1.19.0/go.mod h1:8rVNzU43tQvxDaGvqOhpDqgkJTFowBpDvCJ14kGlJYo=
cloud.google.com/go/speech v1.19.2/go.mod h1:2OYFfj+Ch5LWjsaSINuCZsre/789zlcCI3SY4oAi2oI=
@@ -480,12 +496,17 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8V
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
@@ -501,527 +522,140 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ=
-github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
-github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0=
-github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM=
+github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g=
-github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/getkin/kin-openapi v0.112.0 h1:lnLXx3bAG53EJVI4E/w0N8i1Y/vUZUEsnrXkgnfn7/Y=
-github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
-github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg=
-github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw=
+github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw=
-github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
-github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
-github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
-github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
-github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
-github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
-github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
+github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
-github.com/go-openapi/swag v0.19.13 h1:233UVgMy1DlmCYYfOiFpta6e2urloh+sEs5id6lyzog=
-github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
-github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
-github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
-github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
-github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
-github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
-github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs=
-github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 h1:Ak8CrdlwwXwAZxzS66vgPt4U8yUZX7JwLvVR58FN5jM=
-github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
-github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
-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.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
-github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
-github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
-github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
-github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
-github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
-github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
-github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
-github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
-github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
-github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
-github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
-github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71 h1:24NdJ5N6gtrcoeS4JwLMeruKFmg20QdF/5UnX5S/j18=
github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71/go.mod h1:ozZLfjiLmXytkIUh200wMeuoQJ4ww06wN+KZtFP6j3g=
-github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
-github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
-github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
-github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
-github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
-github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
-github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
-github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
-github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqCDBcAhLXoi3TzC27Zad/Vn+gnVQ=
+github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s=
+github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b/go.mod h1:Bj8LjjP0ReT1eKt5QlKjwgi5AFm5mI6O1A2G4ChI0Ag=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
-github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
-github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
-github.com/smartystreets/assertions v1.2.1 h1:bKNHfEv7tSIjZ8JbKaFjzFINljxG4lzZvmHUnElzOIg=
-github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
-github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
-github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
-github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea h1:CyhwejzVGvZ3Q2PSbQ4NRRYn+ZWv5eS1vlaEusT+bAI=
-go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
-go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs=
-go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM=
-go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
-go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
-go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
-golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
-golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
-golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
+go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
+go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
+go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
+go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgYCza3PXRUGEyCB++y1sAqm6guWFesk=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E=
+go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
+go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
+go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
+go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
+go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+goa.design/goa/v3 v3.14.2 h1:V8skSFWRUTddqZnRy65Mdx0SpGmYe9aQhgCQppyvCKA=
+goa.design/goa/v3 v3.14.2/go.mod h1:sq7F2y/2/eK/XCc8Ff7Was3u42fRmQXp1pTm8FfnGgo=
+goa.design/model v1.9.1 h1:vhY89pjUWW1firAHaccW9MZTiMEEVvtcwN9g8odyywQ=
+goa.design/model v1.9.1/go.mod h1:PlrrmVbrBm7gxqoWqburVUudaMd/d3Sdul7/Kr4Ap54=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
-golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
-golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
-golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
-golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
-golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
-golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
-google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750=
-google.golang.org/api v0.139.0/go.mod h1:CVagp6Eekz9CjGZ718Z+sloknzkDJE7Vc1Ckj9+viBk=
-google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio=
-google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
-google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
-google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8=
-google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
-google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk=
-google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI=
-google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
-google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q=
-google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
-google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0=
-google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870=
-google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
-google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c=
-google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
-google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
-google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
-google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
-google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
-google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
-google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
-google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
-google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
-gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
+golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
+google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
+google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
+google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/log/adapt.go b/log/adapt.go
index 3c9b86d9..e09410ef 100644
--- a/log/adapt.go
+++ b/log/adapt.go
@@ -3,8 +3,10 @@ package log
import (
"context"
"fmt"
+ "sync"
"github.com/aws/smithy-go/logging"
+ "github.com/go-logr/logr"
"goa.design/goa/v3/middleware"
)
@@ -19,16 +21,21 @@ type (
context.Context
}
+ // LogrSink returns a logr LogSink compatible logger.
+ LogrSink struct {
+ lock sync.Mutex
+ name string
+ context.Context
+ }
+
// goaLogger is a Goa middleware compatible logger.
goaLogger struct {
context.Context
}
)
-// Make Goa use clue's request ID context key.
-func init() {
- middleware.RequestIDKey = RequestIDKey
-}
+// NameKey is the key used to log the name of the logger.
+const NameKey = "log"
// AsGoaMiddlewareLogger creates a Goa middleware compatible logger that can be used when
// configuring Goa HTTP or gRPC servers.
@@ -76,6 +83,19 @@ func AsAWSLogger(ctx context.Context) *AWSLogger {
return &AWSLogger{ctx}
}
+// ToLogrSink returns a logr.LogSink.
+//
+// Usage:
+//
+// import "goa.design/clue/log"
+//
+// ctx := log.Context(context.Background())
+// sink := log.ToLogrSink(ctx)
+// logger := logr.New(sink)
+func ToLogrSink(ctx context.Context) *LogrSink {
+ return &LogrSink{Context: ctx}
+}
+
// Fatal is equivalent to l.Print() followed by a call to os.Exit(1).
func (l *StdLogger) Fatal(v ...interface{}) {
l.Print(v...)
@@ -140,6 +160,53 @@ func (l *AWSLogger) WithContext(ctx context.Context) logging.Logger {
return l
}
+func (l *LogrSink) Init(info logr.RuntimeInfo) {}
+
+func (l *LogrSink) Enabled(level int) bool {
+ return true
+}
+
+func (l *LogrSink) Info(level int, msg string, keysAndValues ...interface{}) {
+ kvs := make([]KV, len(keysAndValues)/2+1)
+ kvs[0] = KV{K: "msg", V: msg}
+ for i := 0; i < len(keysAndValues); i += 2 {
+ kvs[i/2+1] = KV{K: fmt.Sprint(keysAndValues[i]), V: keysAndValues[i+1]}
+ }
+ if level == 0 {
+ Info(l, kvList(kvs))
+ } else {
+ Debug(l, kvList(kvs))
+ }
+}
+
+func (l *LogrSink) Error(err error, msg string, keysAndValues ...interface{}) {
+ kvs := make([]KV, len(keysAndValues)/2+1)
+ kvs[0] = KV{K: "msg", V: msg}
+ for i := 0; i < len(keysAndValues); i += 2 {
+ kvs[i/2+1] = KV{K: fmt.Sprint(keysAndValues[i]), V: keysAndValues[i+1]}
+ }
+ Error(l, err, kvList(kvs))
+}
+
+func (l *LogrSink) WithValues(keysAndValues ...any) logr.LogSink {
+ kvs := make([]KV, len(keysAndValues)/2)
+ for i := 0; i < len(keysAndValues); i += 2 {
+ kvs[i/2] = KV{K: fmt.Sprint(keysAndValues[i]), V: keysAndValues[i+1]}
+ }
+ return &LogrSink{Context: With(l, kvList(kvs))}
+}
+
+func (l *LogrSink) WithName(name string) logr.LogSink {
+ l.lock.Lock()
+ defer l.lock.Unlock()
+ cur := l.name
+ if cur != "" {
+ cur += "/"
+ }
+ l.name = cur + name
+ return &LogrSink{Context: With(l, KV{NameKey, l.name}), name: l.name}
+}
+
// Log creates a log entry using a sequence of key/value pairs.
func (l goaLogger) Log(keyvals ...interface{}) error {
n := (len(keyvals) + 1) / 2
diff --git a/log/adapt_test.go b/log/adapt_test.go
index 41b47b26..a1f4c645 100644
--- a/log/adapt_test.go
+++ b/log/adapt_test.go
@@ -3,10 +3,12 @@ package log
import (
"bytes"
"context"
+ "errors"
"testing"
"time"
"github.com/aws/smithy-go/logging"
+ "github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
)
@@ -131,3 +133,67 @@ func TestAsAWSLogger(t *testing.T) {
want = "time=2022-01-09T20:29:45Z level=info msg=\"hello small world\"\n"
assert.Equal(t, buf.String(), want)
}
+
+func TestToLogrSink(t *testing.T) {
+ restore := timeNow
+ timeNow = func() time.Time { return time.Date(2022, time.January, 9, 20, 29, 45, 0, time.UTC) }
+ defer func() { timeNow = restore }()
+ var buf bytes.Buffer
+ ctx := Context(context.Background(), WithOutput(&buf), WithDebug())
+ var sink logr.LogSink = ToLogrSink(ctx)
+
+ sink.Init(logr.RuntimeInfo{})
+ assert.True(t, sink.Enabled(0))
+ assert.True(t, sink.Enabled(1))
+ assert.True(t, sink.Enabled(2))
+ assert.True(t, sink.Enabled(3))
+
+ msg := "hello world"
+ expected := "time=2022-01-09T20:29:45Z level=info msg=\"hello world\"\n"
+ expecteddebug := "time=2022-01-09T20:29:45Z level=debug msg=\"hello world\"\n"
+ expectederr := "time=2022-01-09T20:29:45Z level=error err=error msg=\"hello world\"\n"
+ empty := ""
+
+ logger := logr.New(sink)
+ logger.Info(msg)
+ assert.Equal(t, expected, buf.String())
+
+ buf.Reset()
+ logger.V(1).Info(msg)
+ assert.Equal(t, expecteddebug, buf.String())
+
+ buf.Reset()
+ logger.Error(errors.New("error"), msg)
+ assert.Equal(t, expectederr, buf.String())
+
+ ctx = Context(context.Background(), WithOutput(&buf))
+ sink = ToLogrSink(ctx)
+ logger = logr.New(sink)
+
+ buf.Reset()
+ logger.Info(msg)
+ assert.Equal(t, empty, buf.String())
+
+ FlushAndDisableBuffering(ctx)
+ buf.Reset()
+ logger.Info(msg)
+ assert.Equal(t, expected, buf.String())
+
+ buf.Reset()
+ logger.V(1).Info(msg)
+ assert.Equal(t, empty, buf.String())
+
+ sink = sink.WithValues("key", "value")
+ expectedWithValues := "time=2022-01-09T20:29:45Z level=info key=value msg=\"hello world\"\n"
+ logger = logr.New(sink)
+ buf.Reset()
+ logger.Info(msg)
+ assert.Equal(t, expectedWithValues, buf.String())
+
+ sink = sink.WithName("name")
+ expectedWithName := "time=2022-01-09T20:29:45Z level=info key=value log=name msg=\"hello world\"\n"
+ logger = logr.New(sink)
+ buf.Reset()
+ logger.Info(msg)
+ assert.Equal(t, expectedWithName, buf.String())
+}
diff --git a/log/grpc.go b/log/grpc.go
index 5154f4e7..a1f7c5a6 100644
--- a/log/grpc.go
+++ b/log/grpc.go
@@ -2,10 +2,12 @@ package log
import (
"context"
+ "crypto/rand"
+ "encoding/base64"
+ "io"
"path"
"time"
- goamiddleware "goa.design/goa/v3/middleware"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -24,9 +26,13 @@ type (
grpcOptions struct {
iserr func(codes.Code) bool
disableCallLogging bool
+ disableCallID bool
}
)
+// Be nice to tests
+var shortID = randShortID
+
// UnaryServerInterceptor returns a unary interceptor that performs two tasks:
// 1. Enriches the request context with the logger specified in logCtx.
// 2. Logs details of the unary call, unless the WithDisableCallLogging option is provided.
@@ -44,8 +50,8 @@ func UnaryServerInterceptor(logCtx context.Context, opts ...GRPCLogOption) grpc.
handler grpc.UnaryHandler,
) (interface{}, error) {
ctx = WithContext(ctx, logCtx)
- if reqID := ctx.Value(goamiddleware.RequestIDKey); reqID != nil {
- ctx = With(ctx, KV{RequestIDKey, reqID})
+ if !o.disableCallID {
+ ctx = With(ctx, KV{RequestIDKey, shortID()})
}
if o.disableCallLogging {
return handler(ctx, req)
@@ -88,8 +94,8 @@ func StreamServerInterceptor(logCtx context.Context, opts ...GRPCLogOption) grpc
handler grpc.StreamHandler,
) error {
ctx := WithContext(stream.Context(), logCtx)
- if reqID := ctx.Value(goamiddleware.RequestIDKey); reqID != nil {
- ctx = With(ctx, KV{RequestIDKey, reqID})
+ if !o.disableCallID {
+ ctx = With(ctx, KV{RequestIDKey, shortID()})
}
stream = &streamWithContext{stream, ctx}
if o.disableCallLogging {
@@ -204,6 +210,14 @@ func WithDisableCallLogging() GRPCLogOption {
}
}
+// WithDisableCallID returns a GRPC logger option that disables the
+// generation of request IDs.
+func WithDisableCallID() GRPCLogOption {
+ return func(o *grpcOptions) {
+ o.disableCallID = true
+ }
+}
+
func defaultGRPCOptions() *grpcOptions {
return &grpcOptions{
iserr: func(c codes.Code) bool {
@@ -220,3 +234,11 @@ type streamWithContext struct {
func (s *streamWithContext) Context() context.Context {
return s.ctx
}
+
+// randShortID produces a "unique" 6 bytes long string.
+// This algorithm favors simplicity and efficiency over true uniqueness.
+func randShortID() string {
+ b := make([]byte, 6)
+ io.ReadFull(rand.Reader, b) // nolint: errcheck
+ return base64.RawURLEncoding.EncodeToString(b)
+}
diff --git a/log/grpc_test.go b/log/grpc_test.go
index 3e824f18..31e37786 100644
--- a/log/grpc_test.go
+++ b/log/grpc_test.go
@@ -12,8 +12,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"goa.design/clue/internal/testsvc"
- grpcmiddleware "goa.design/goa/v3/grpc/middleware"
- goamiddleware "goa.design/goa/v3/middleware"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -24,13 +22,13 @@ func TestUnaryServerInterceptor(t *testing.T) {
timeNow = func() time.Time { return time.Date(2022, time.January, 9, 20, 29, 45, 0, time.UTC) }
defer func() { timeNow = now }()
- testRequestID := "test-request-id"
- requestIDInterceptor := unaryStaticRequestIDInterceptor(testRequestID)
+ shortID = func() string { return "test-request-id" }
+ defer func() { shortID = randShortID }()
- prefix := `{"time":"2022-01-09T20:29:45Z","level":"info","request-id":"test-request-id","msg":"start","grpc.service":"test.Test","grpc.method":"GrpcMethod"}`
- logged := `{"time":"2022-01-09T20:29:45Z","level":"info","request-id":"test-request-id","key1":"value1","key2":"value2"}`
- suffix := `{"time":"2022-01-09T20:29:45Z","level":"info","request-id":"test-request-id","msg":"end","grpc.service":"test.Test","grpc.method":"GrpcMethod","grpc.code":"OK","grpc.time_ms":0}`
- errors := `{"time":"2022-01-09T20:29:45Z","level":"error","request-id":"test-request-id","err":"rpc error: code = Unknown desc = test-error","grpc.service":"test.Test","grpc.method":"GrpcMethod","grpc.status":"test-error","grpc.code":"Unknown","grpc.time_ms":0}`
+ prefix := `{"time":"2022-01-09T20:29:45Z","level":"info","request_id":"test-request-id","msg":"start","grpc.service":"test.Test","grpc.method":"GrpcMethod"}`
+ logged := `{"time":"2022-01-09T20:29:45Z","level":"info","request_id":"test-request-id","key1":"value1","key2":"value2"}`
+ suffix := `{"time":"2022-01-09T20:29:45Z","level":"info","request_id":"test-request-id","msg":"end","grpc.service":"test.Test","grpc.method":"GrpcMethod","grpc.code":"OK","grpc.time_ms":0}`
+ errors := `{"time":"2022-01-09T20:29:45Z","level":"error","request_id":"test-request-id","err":"rpc error: code = Unknown desc = test-error","grpc.service":"test.Test","grpc.method":"GrpcMethod","grpc.status":"test-error","grpc.code":"Unknown","grpc.time_ms":0}`
cases := []struct {
name string
@@ -65,7 +63,7 @@ func TestUnaryServerInterceptor(t *testing.T) {
ctx := Context(context.Background(), WithOutput(&buf), WithFormat(FormatJSON))
logInterceptor := UnaryServerInterceptor(ctx, c.options...)
cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.ChainUnaryInterceptor(requestIDInterceptor, logInterceptor)),
+ testsvc.WithServerOptions(grpc.UnaryInterceptor(logInterceptor)),
testsvc.WithUnaryFunc(c.method))
_, err := cli.GRPCMethod(context.Background(), &testsvc.Fields{})
@@ -87,13 +85,13 @@ func TestStreamServerTrace(t *testing.T) {
timeNow = func() time.Time { return time.Date(2022, time.January, 9, 20, 29, 45, 0, time.UTC) }
defer func() { timeNow = now }()
- testRequestID := "test-request-id"
- requestIDInterceptor := streamStaticRequestIDInterceptor(testRequestID)
+ shortID = func() string { return "test-request-id" }
+ defer func() { shortID = randShortID }()
- prefix := `{"time":"2022-01-09T20:29:45Z","level":"info","request-id":"test-request-id","msg":"start","grpc.service":"test.Test","grpc.method":"GrpcStream"}`
- logged := `{"time":"2022-01-09T20:29:45Z","level":"info","request-id":"test-request-id","key1":"value1","key2":"value2"}`
- suffix := `{"time":"2022-01-09T20:29:45Z","level":"info","request-id":"test-request-id","msg":"end","grpc.service":"test.Test","grpc.method":"GrpcStream","grpc.code":"OK","grpc.time_ms":XXX}`
- errors := `{"time":"2022-01-09T20:29:45Z","level":"error","request-id":"test-request-id","err":"rpc error: code = Unknown desc = test-error","grpc.service":"test.Test","grpc.method":"GrpcStream","grpc.status":"test-error","grpc.code":"Unknown","grpc.time_ms":XXX}`
+ prefix := `{"time":"2022-01-09T20:29:45Z","level":"info","request_id":"test-request-id","msg":"start","grpc.service":"test.Test","grpc.method":"GrpcStream"}`
+ logged := `{"time":"2022-01-09T20:29:45Z","level":"info","request_id":"test-request-id","key1":"value1","key2":"value2"}`
+ suffix := `{"time":"2022-01-09T20:29:45Z","level":"info","request_id":"test-request-id","msg":"end","grpc.service":"test.Test","grpc.method":"GrpcStream","grpc.code":"OK","grpc.time_ms":XXX}`
+ errors := `{"time":"2022-01-09T20:29:45Z","level":"error","request_id":"test-request-id","err":"rpc error: code = Unknown desc = test-error","grpc.service":"test.Test","grpc.method":"GrpcStream","grpc.status":"test-error","grpc.code":"Unknown","grpc.time_ms":XXX}`
cases := []struct {
name string
@@ -128,7 +126,7 @@ func TestStreamServerTrace(t *testing.T) {
ctx := Context(context.Background(), WithOutput(&buf), WithFormat(FormatJSON))
logInterceptor := StreamServerInterceptor(ctx, c.options...)
cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.ChainStreamInterceptor(requestIDInterceptor, logInterceptor)),
+ testsvc.WithServerOptions(grpc.StreamInterceptor(logInterceptor)),
testsvc.WithStreamFunc(c.method))
stream, err := cli.GRPCStream(context.Background())
@@ -156,6 +154,7 @@ func TestUnaryClientInterceptor(t *testing.T) {
endLog := `time=2022-01-09T20:29:45Z level=info msg=end grpc.service=test.Test grpc.method=GrpcMethod grpc.code=OK grpc.time_ms=42`
errorLog := `time=2022-01-09T20:29:45Z level=error err="rpc error: code = Unknown desc = error" grpc.service=test.Test grpc.method=GrpcMethod grpc.status=error grpc.code=Unknown grpc.time_ms=42`
statusLog := `time=2022-01-09T20:29:45Z level=error err="rpc error: code = Unknown desc = error" grpc.service=test.Test grpc.method=GrpcMethod grpc.status=error grpc.code=Unknown grpc.time_ms=42`
+
cases := []struct {
name string
noLog bool
@@ -219,6 +218,7 @@ func TestUnaryClientInterceptor(t *testing.T) {
func TestStreamClientInterceptor(t *testing.T) {
startLog := `time=2022-01-09T20:29:45Z level=info msg=start grpc.service=test.Test grpc.method=GrpcStream`
endLog := `time=2022-01-09T20:29:45Z level=info msg=end grpc.service=test.Test grpc.method=GrpcStream grpc.code=OK grpc.time_ms=42`
+
cases := []struct {
name string
noLog bool
@@ -259,8 +259,7 @@ func TestStreamClientInterceptor(t *testing.T) {
func logUnaryMethod(ctx context.Context, _ *testsvc.Fields) (*testsvc.Fields, error) {
Print(ctx, KV{"key1", "value1"}, KV{"key2", "value2"})
- reqID := ctx.Value(goamiddleware.RequestIDKey).(string)
- return &testsvc.Fields{S: &reqID}, nil
+ return &testsvc.Fields{}, nil
}
func errorMethod(ctx context.Context, _ *testsvc.Fields) (*testsvc.Fields, error) {
@@ -273,8 +272,6 @@ func echoMethod(ctx context.Context, stream testsvc.Stream) (err error) {
if err != nil {
return err
}
- reqID := ctx.Value(goamiddleware.RequestIDKey).(string)
- f.S = &reqID
if err := stream.Send(f); err != nil {
return err
}
@@ -299,18 +296,3 @@ func dummyStreamMethod() func(context.Context, testsvc.Stream) error {
return stream.Close()
}
}
-
-func unaryStaticRequestIDInterceptor(id string) grpc.UnaryServerInterceptor {
- return func(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
- ctx = context.WithValue(ctx, goamiddleware.RequestIDKey, id) // nolint:staticcheck
- return handler(ctx, req)
- }
-}
-
-func streamStaticRequestIDInterceptor(id string) grpc.StreamServerInterceptor {
- return func(srv interface{}, stream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
- ctx := context.WithValue(stream.Context(), goamiddleware.RequestIDKey, id) // nolint:staticcheck
- wss := grpcmiddleware.NewWrappedServerStream(ctx, stream)
- return handler(srv, wss)
- }
-}
diff --git a/log/http.go b/log/http.go
index 9f3363ac..b8a3f719 100644
--- a/log/http.go
+++ b/log/http.go
@@ -1,16 +1,16 @@
package log
import (
+ "bufio"
"bytes"
"context"
+ "errors"
"fmt"
"io"
"net"
"net/http"
"regexp"
- goahttpmiddleware "goa.design/goa/v3/http/middleware"
- "goa.design/goa/v3/middleware"
goa "goa.design/goa/v3/pkg"
)
@@ -26,6 +26,7 @@ type (
httpLogOptions struct {
pathFilters []*regexp.Regexp
disableRequestLogging bool
+ disableRequestID bool
}
httpClientOptions struct {
@@ -38,6 +39,14 @@ type (
http.RoundTripper
options *httpClientOptions
}
+
+ // responseCapture is a http.ResponseWriter which captures the response status
+ // code and content length.
+ responseCapture struct {
+ http.ResponseWriter
+ StatusCode int
+ ContentLength int
+ }
)
// HTTP returns a HTTP middleware that performs two tasks:
@@ -63,8 +72,8 @@ func HTTP(logCtx context.Context, opts ...HTTPLogOption) func(http.Handler) http
}
}
ctx := WithContext(req.Context(), logCtx)
- if requestID := req.Context().Value(middleware.RequestIDKey); requestID != nil {
- ctx = With(ctx, KV{RequestIDKey, requestID})
+ if !options.disableRequestID {
+ ctx = With(ctx, KV{RequestIDKey, shortID()})
}
if options.disableRequestLogging {
h.ServeHTTP(w, req.WithContext(ctx))
@@ -75,7 +84,7 @@ func HTTP(logCtx context.Context, opts ...HTTPLogOption) func(http.Handler) http
fromKV := KV{K: HTTPFromKey, V: from(req)}
Print(ctx, KV{K: MessageKey, V: "start"}, methKV, urlKV, fromKV)
- rw := goahttpmiddleware.CaptureResponse(w)
+ rw := &responseCapture{ResponseWriter: w}
started := timeNow()
h.ServeHTTP(rw, req.WithContext(ctx))
@@ -146,6 +155,14 @@ func WithDisableRequestLogging() HTTPLogOption {
}
}
+// WithDisableRequestID returns a HTTP middleware option that disables the
+// generation of request IDs.
+func WithDisableRequestID() HTTPLogOption {
+ return func(o *httpLogOptions) {
+ o.disableRequestID = true
+ }
+}
+
// RoundTrip executes the given HTTP request and logs the request and response. The
// request context must be initialized with a clue logger.
func (c *client) RoundTrip(req *http.Request) (resp *http.Response, err error) {
@@ -179,6 +196,44 @@ func (c *client) RoundTrip(req *http.Request) (resp *http.Response, err error) {
return
}
+// WriteHeader records the value of the status code before writing it.
+func (w *responseCapture) WriteHeader(code int) {
+ w.StatusCode = code
+ w.ResponseWriter.WriteHeader(code)
+}
+
+// Write computes the written len and stores it in ContentLength.
+func (w *responseCapture) Write(b []byte) (int, error) {
+ n, err := w.ResponseWriter.Write(b)
+ w.ContentLength += n
+ return n, err
+}
+
+// Flush implements the http.Flusher interface if the underlying response
+// writer supports it.
+func (w *responseCapture) Flush() {
+ if f, ok := w.ResponseWriter.(http.Flusher); ok {
+ f.Flush()
+ }
+}
+
+// Push implements the http.Pusher interface if the underlying response
+// writer supports it.
+func (w *responseCapture) Push(target string, opts *http.PushOptions) error {
+ if p, ok := w.ResponseWriter.(http.Pusher); ok {
+ return p.Push(target, opts)
+ }
+ return errors.New("push not supported")
+}
+
+// Hijack supports the http.Hijacker interface.
+func (w *responseCapture) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+ if h, ok := w.ResponseWriter.(http.Hijacker); ok {
+ return h.Hijack()
+ }
+ return nil, nil, fmt.Errorf("response writer does not support hijacking: %T", w.ResponseWriter)
+}
+
// from returns the client address from the request.
func from(req *http.Request) string {
if f := req.Header.Get("X-Forwarded-For"); f != "" {
diff --git a/log/http_test.go b/log/http_test.go
index 2907fd45..92d5fc53 100644
--- a/log/http_test.go
+++ b/log/http_test.go
@@ -12,7 +12,6 @@ import (
"time"
"github.com/stretchr/testify/assert"
- "goa.design/goa/v3/middleware"
goa "goa.design/goa/v3/pkg"
)
@@ -22,10 +21,12 @@ func TestHTTP(t *testing.T) {
defer func() { timeNow = now }()
timeSince = func(_ time.Time) time.Duration { return 42 * time.Millisecond }
defer func() { timeSince = time.Since }()
+ shortID = func() string { return "test-request-id" }
+ defer func() { shortID = randShortID }()
- prefix := `{"time":"2022-01-09T20:29:45Z","level":"info","request-id":"request-id","msg":"start","http.method":"GET","http.url":"http://example.com","http.remote_addr":""}`
- entry := `{"time":"2022-01-09T20:29:45Z","level":"info","request-id":"request-id","key1":"value1","key2":"value2"}`
- suffix := `{"time":"2022-01-09T20:29:45Z","level":"info","request-id":"request-id","msg":"end","http.method":"GET","http.url":"http://example.com","http.status":0,"http.time_ms":42,"http.bytes":0}`
+ prefix := `{"time":"2022-01-09T20:29:45Z","level":"info","request_id":"test-request-id","msg":"start","http.method":"GET","http.url":"http://example.com","http.remote_addr":""}`
+ entry := `{"time":"2022-01-09T20:29:45Z","level":"info","request_id":"test-request-id","key1":"value1","key2":"value2"}`
+ suffix := `{"time":"2022-01-09T20:29:45Z","level":"info","request_id":"test-request-id","msg":"end","http.method":"GET","http.url":"http://example.com","http.status":0,"http.time_ms":42,"http.bytes":0}`
cases := []struct {
name string
@@ -56,9 +57,7 @@ func TestHTTP(t *testing.T) {
handler = HTTP(ctx, c.opt)(handler)
- requestIDCtx := context.WithValue(context.Background(), middleware.RequestIDKey, "request-id") //nolint:staticcheck
req, _ := http.NewRequest("GET", "http://example.com", nil)
- req = req.WithContext(requestIDCtx)
handler.ServeHTTP(nil, req)
diff --git a/log/keys.go b/log/keys.go
index 5f0e5578..0b6c7a2e 100644
--- a/log/keys.go
+++ b/log/keys.go
@@ -1,9 +1,9 @@
package log
var (
- TraceIDKey = "trace-id"
- SpanIDKey = "span-id"
- RequestIDKey = "request-id"
+ TraceIDKey = "trace_id"
+ SpanIDKey = "span_id"
+ RequestIDKey = "request_id"
MessageKey = "msg"
ErrorMessageKey = "err"
TimestampKey = "time"
diff --git a/log/span.go b/log/span.go
new file mode 100644
index 00000000..d1ac6cb4
--- /dev/null
+++ b/log/span.go
@@ -0,0 +1,23 @@
+package log
+
+import (
+ "context"
+
+ "go.opentelemetry.io/otel/trace"
+)
+
+// Log is a log key/value pair generator function that can be used to log trace
+// and span IDs. Example:
+//
+// ctx := log.Context(ctx, WithFunc(trace.Log))
+// log.Printf(ctx, "message")
+//
+// Output: traceID= spanID= message
+func Span(ctx context.Context) (kvs []KV) {
+ spanContext := trace.SpanFromContext(ctx).SpanContext()
+ if spanContext.IsValid() {
+ kvs = append(kvs, KV{K: TraceIDKey, V: spanContext.TraceID().String()})
+ kvs = append(kvs, KV{K: SpanIDKey, V: spanContext.SpanID().String()})
+ }
+ return
+}
diff --git a/log/span_test.go b/log/span_test.go
new file mode 100644
index 00000000..1fbb6527
--- /dev/null
+++ b/log/span_test.go
@@ -0,0 +1,32 @@
+package log
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "go.opentelemetry.io/otel/trace"
+)
+
+func TestSpan(t *testing.T) {
+ // Create a mock span context
+ traceID := trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
+ spanID := trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8}
+ spanContext := trace.NewSpanContext(trace.SpanContextConfig{
+ TraceID: traceID,
+ SpanID: spanID,
+ TraceFlags: trace.FlagsSampled,
+ })
+
+ // Create a context with the mock span context
+ ctx := trace.ContextWithSpanContext(context.Background(), spanContext)
+
+ // Call the Span function
+ kvs := Span(ctx)
+
+ // Assert that the expected key-value pairs are returned
+ assert.Equal(t, []KV{
+ {K: TraceIDKey, V: "0102030405060708090a0b0c0d0e0f10"},
+ {K: SpanIDKey, V: "0102030405060708"},
+ }, kvs)
+}
diff --git a/metrics/README.md b/metrics/README.md
deleted file mode 100644
index 117a77d0..00000000
--- a/metrics/README.md
+++ /dev/null
@@ -1,152 +0,0 @@
-# metrics: Auto Metrics
-
-[![Build Status](https://github.com/goadesign/clue/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/goadesign/clue/actions?query=branch%3Amain+event%3Apush)
-[![Go Reference](https://pkg.go.dev/badge/goa.design/clue/metrics.svg)](https://pkg.go.dev/goa.design/clue/metrics)
-
-## Overview
-
-Package `metrics` provides a convenient way to add Prometheus metrics to Goa
-services. The following example shows how to use the package. It implements an
-illustrative `main` function for a fictional service `svc` implemented in the
-package `github.com/repo/services/svc`. The service is assumed to expose both
-HTTP and gRPC endpoints.
-
-```go
-package main
-
-import (
- "context"
-
- "goa.design/clue/metrics"
- "goa.design/clue/log"
- goahttp "goa.design/goa/v3/http"
-
- "github.com/repo/services/svc"
- httpsvrgen "github.com/repo/services/svc/gen/http/svc/server"
- grpcsvrgen "github.com/repo/services/svc/gen/grpc/svc/server"
- svcgen "github.com/repo/services/svc/gen/svc"
-)
-
-func main() {
- // Initialize the log context
- ctx := log.With(log.Context(context.Background()), "svc", svcgen.ServiceName)
- // Create the service (user code)
- svc := svc.New(ctx)
- // Wrap the service with Goa endpoints
- endpoints := svcgen.NewEndpoints(svc)
-
- // Create HTTP server
- mux := goahttp.NewMuxer()
- httpsvr := httpsvrgen.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil)
- httpsvrgen.Mount(mux, httpsvr)
-
- // ** Initialize context for metrics **
- ctx = metrics.Context(ctx, svcgen.ServiceName)
-
- // ** metrics HTTP endpoints **
- handler := metrics.HTTP(ctx)(mux)
-
- // Create gRPC server
- grpcsvr := grpcsvrgen.New(endpoints, nil)
-
- // ** metrics gRPC endpoints **
- unaryInterceptor := metrics.UnaryServerInterceptor(ctx)
- streamInterceptor := metrics.StreamServerInterceptor(ctx)
- pbsvr := grpc.NewServer(grpc.UnaryInterceptor(unaryInterceptor), grpc.StreamInterceptor(streamInterceptor))
-
- // ** Mount the /metrics handler used by Prometheus to scrape metrics **
- mux.Handle("GET", "/metrics", metrics.Handler(ctx).ServeHTTP)
-
- // .... Start the servers ....
-}
-```
-
-The `metrics` functions used to instrument the service are:
-
-* `HTTP`: creates a middleware that meters an HTTP handler.
-* `UnaryServerInterceptor`: creates an interceptor that meters gRPC unary server methods.
-* `StreamServerInterceptor`: creates an interceptor that meters gRPC stream server methods.
-* `Handler`: creates a HTTP handler that exposes Prometheus metrics.
-
-## HTTP Metrics
-
-The middleware returned by the `HTTP` function creates the following metrics:
-
-* `http_server_duration`: Histogram of HTTP request durations in milliseconds.
-* `http_server_active_requests`: Gauge of active HTTP requests.
-* `http_server_request_size`: Histogram of HTTP request sizes in bytes.
-* `http_server_response_size`: Histogram of HTTP response sizes in bytes.
-
-All the metrics have the following labels:
-
-* `goa_service`: The service name as specified in the Goa design.
-* `http_verb`: The HTTP verb (`GET`, `POST` etc.).
-* `http_host`: The value of the HTTP host header.
-* `http_path`: The HTTP path.
-
-All the metrics but `http_server_active_requests` also have the following
-additional labels:
-
-* `http_status_code`: The HTTP status code.
-
-## GRPC Metrics
-
-The `UnaryInterceptor` and `StreamInterceptor` functions create the following
-metrics:
-
-* `rpc_server_duration`: Histogram of unary request durations in milliseconds.
-* `rpc_server_active_requests`: Gauge of active unary and stream requests.
-* `rpc_server_request_size`: Histogram of message sizes in bytes, per message for streaming RPCs.
-* `rpc_server_response_size`: Histogram of response sizes in bytes, per message for streaming RPCs.
-* `rpc_server_stream_message_size`: Histogram of message sizes in bytes, per message for streaming RPCs.
-* `rpc_server_stream_response_size`: Histogram of response sizes in bytes, per message for streaming RPCs.
-
-All the metrics have the following labels:
-
-* `goa_service`: The service name as specified in the Goa design.
-* `net_peer.addr`: The peer address.
-* `rpc_method`: Full name of RPC method.
-
-All the metrics but `rpc_server_active_requests`,
-`rpc_server_stream_message_size` and `rpc_rpc_server_stream_response_size` also
-have the following additional labels:
-
-* `rpc_status_code`: The response status code.
-
-## Configuration
-
-### Histogram Buckets
-
-The histogram buckets can be specified using the `WithDurationBuckets`,
-`WithRequestSizeBuckets` and `WithResponseSizeBuckets` options of the `Context`
-function:
-
-```go
-ctx = metrics.Context(ctx, svc.ServiceName,
- metrics.WithDurationBuckets([]float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}),
- metrics.WithRequestSizeBuckets([]float64{1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}),
- metrics.WithResponseSizeBuckets([]float64{1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}))
-```
-
-The default bucket upper boundaries are:
-
-* `10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, +Inf` for request duration.
-* `10, 100, 500, 1000, 5000, 10000, 50000, 100000, 1000000, 10000000, +Inf` for request and response size.
-
-### Prometheus Registry
-
-By default `metrics` uses the global Prometheus registry to create the
-metrics and serve them. A user configured registerer can be specified when
-creating the metrics via `WithRegisterer`:
-
-```go
-ctx = metrics.Context(ctx, svc.ServiceName, metrics.WithRegisterer(registerer))(mux)
-```
-
-A user configured gatherer (used to collect the metrics) and registerer (used to
-register metrics for the `/metrics` endpoint) can be specified when creating the
-metrics handler via `WithGatherer` and `WithHandlerRegisterer` respectively:
-
-```go
-handler = metrics.Handler(ctx, metrics.WithGatherer(gatherer), metrics.WithHandlerRegisterer(registerer))
-```
diff --git a/metrics/context.go b/metrics/context.go
deleted file mode 100644
index b301a4ab..00000000
--- a/metrics/context.go
+++ /dev/null
@@ -1,239 +0,0 @@
-package metrics
-
-import (
- "context"
-
- "github.com/prometheus/client_golang/prometheus"
-)
-
-type (
- // stateBag is stored in the context and used to initialize the
- // appropriate metrics for this package HTTP middleware and gRPC
- // interceptors. This state is only needed during initialization and is
- // not intended to be kept in request contexts.
- stateBag struct {
- options *options
- svc string
- httpMetrics *httpMetrics
- grpcMetrics *grpcMetrics
- }
-
- // httpMetrics is the set of HTTP Metrics used by this package interceptors.
- httpMetrics struct {
- // Durations is a histogram of the duration of requests.
- Durations *prometheus.HistogramVec
- // RequestSizes is a histogram of the size of requests.
- RequestSizes *prometheus.HistogramVec
- // ResponseSizes is a histogram of the size of responses.
- ResponseSizes *prometheus.HistogramVec
- // ActiveRequests is a gauge of the number of active requests.
- ActiveRequests *prometheus.GaugeVec
- }
-
- // grpcMetrics is the set of gRPC Metrics used by this package interceptors.
- grpcMetrics struct {
- // Durations is a histogram of the duration of requests.
- Durations *prometheus.HistogramVec
- // RequestSizes is a histogram of the size of requests.
- RequestSizes *prometheus.HistogramVec
- // ResponseSizes is a histogram of the size of responses.
- ResponseSizes *prometheus.HistogramVec
- // ActiveRequests is a gauge of the number of active requests.
- ActiveRequests *prometheus.GaugeVec
- // StreamMessageSizes is a histogram of the size of messages sent on the stream.
- StreamMessageSizes *prometheus.HistogramVec
- // StreamResultSizes is a histogram of the size of results sent on the stream.
- StreamResultSizes *prometheus.HistogramVec
- }
-
- // Private type used to define context keys.
- ctxKey int
-)
-
-const (
- // metricHTTPDuration is the name of the HTTP duration metric.
- metricHTTPDuration = "http_server_duration_ms"
- // metricHTTPRequestSize is the name of the HTTP request size metric.
- metricHTTPRequestSize = "http_server_request_size_bytes"
- // metricHTTPResponseSize is the name of the HTTP response size metric.
- metricHTTPResponseSize = "http_server_response_size_bytes"
- // metricHTTPActiveRequests is the name of the HTTP active requests metric.
- metricHTTPActiveRequests = "http_server_active_requests"
- // metricRPCDuration is the name of the gRPC request duration metric.
- metricRPCDuration = "rpc_server_duration_ms"
- // metricRPCActiveRequests is the name of the gRPC active requests metric.
- metricRPCActiveRequests = "rpc_server_active_requests"
- // metricRPCRequestSize is the name of the gRPC request size metric.
- metricRPCRequestSize = "rpc_server_request_size_bytes"
- // metricRPCResponseSize is the name of the gRPC response size metric.
- metricRPCResponseSize = "rpc_server_response_size_bytes"
- // metricRPCStreamMessageSize is the name of the gRPC stream message size metric.
- metricRPCStreamMessageSize = "rpc_server_stream_message_size_bytes"
- // metricRPCStreamResponseSize is the name of the gRPC stream response size metric.
- metricRPCStreamResponseSize = "rpc_server_stream_response_size_bytes"
- // labelGoaService is the name of the label containing the Goa service name.
- labelGoaService = "goa_service"
- // labelHTTPVerb is the name of the label containing the HTTP verb.
- labelHTTPVerb = "http_verb"
- // labelHTTPHost is the name of the label containing the HTTP host.
- labelHTTPHost = "http_host"
- // labelHTTPPath is the name of the label containing the HTTP URL path.
- labelHTTPPath = "http_path"
- // labelHTTPStatusCode is the name of the label containing the HTTP status code.
- labelHTTPStatusCode = "http_status_code"
- // labelRPCService is the name of the RPC service label.
- labelRPCService = "rpc_service"
- // labelRPCMethod is the name of the RPC method label.
- labelRPCMethod = "rpc_method"
- // labelRPCStatusCode is the name of the RPC status code label.
- labelRPCStatusCode = "rpc_status_code"
-)
-
-const (
- // Context key used to capture request length.
- ctxReqLen ctxKey = iota + 1
- // Context key used to store initialization state bag.
- stateBagKey
-)
-
-var (
- // httpLabels is the set of dynamic labels used for all metrics but
- // MetricHTTPActiveRequests.
- httpLabels = []string{labelHTTPVerb, labelHTTPHost, labelHTTPPath, labelHTTPStatusCode}
-
- // httpActiveRequestsLabels is the set of dynamic labels used for the
- // MetricHTTPActiveRequests metric.
- httpActiveRequestsLabels = []string{labelHTTPVerb, labelHTTPHost, labelHTTPPath}
-
- // rpcLabels is the default set of dynamic metric labels
- rpcLabels = []string{labelRPCService, labelRPCMethod, labelRPCStatusCode}
-
- // NoCode is the set of dynamic labels used for active gRPC requests
- // metric and stream message and result size metrics.
- rpcNoCodeLabels = []string{labelRPCService, labelRPCMethod}
-)
-
-// Context initializes the given context for the HTTP, UnaryInterceptor and
-// StreamInterceptor functions.
-func Context(ctx context.Context, svc string, opts ...Option) context.Context {
- if m := ctx.Value(stateBagKey); m != nil {
- return ctx
- }
-
- options := defaultOptions()
- for _, o := range opts {
- o(options)
- }
-
- return context.WithValue(ctx, stateBagKey, &stateBag{options: options, svc: svc})
-}
-
-func (state *stateBag) HTTPMetrics() *httpMetrics {
- if state.httpMetrics != nil {
- return state.httpMetrics
- }
-
- durations := prometheus.NewHistogramVec(prometheus.HistogramOpts{
- Name: metricHTTPDuration,
- Help: "Histogram of request durations in milliseconds.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- Buckets: state.options.durationBuckets,
- }, httpLabels)
- state.options.registerer.MustRegister(durations)
-
- reqSizes := prometheus.NewHistogramVec(prometheus.HistogramOpts{
- Name: metricHTTPRequestSize,
- Help: "Histogram of request sizes in bytes.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- Buckets: state.options.requestSizeBuckets,
- }, httpLabels)
- state.options.registerer.MustRegister(reqSizes)
-
- respSizes := prometheus.NewHistogramVec(prometheus.HistogramOpts{
- Name: metricHTTPResponseSize,
- Help: "Histogram of response sizes in bytes.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- Buckets: state.options.responseSizeBuckets,
- }, httpLabels)
- state.options.registerer.MustRegister(respSizes)
-
- activeReqs := prometheus.NewGaugeVec(prometheus.GaugeOpts{
- Name: metricHTTPActiveRequests,
- Help: "Gauge of active requests.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- }, httpActiveRequestsLabels)
- state.options.registerer.MustRegister(activeReqs)
-
- state.httpMetrics = &httpMetrics{
- Durations: durations,
- RequestSizes: reqSizes,
- ResponseSizes: respSizes,
- ActiveRequests: activeReqs,
- }
-
- return state.httpMetrics
-}
-
-func (state *stateBag) GRPCMetrics() *grpcMetrics {
- if state.grpcMetrics != nil {
- return state.grpcMetrics
- }
-
- durations := prometheus.NewHistogramVec(prometheus.HistogramOpts{
- Name: metricRPCDuration,
- Help: "Histogram of request durations in milliseconds.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- Buckets: state.options.durationBuckets,
- }, rpcLabels)
- state.options.registerer.MustRegister(durations)
-
- reqSizes := prometheus.NewHistogramVec(prometheus.HistogramOpts{
- Name: metricRPCRequestSize,
- Help: "Histogram of request sizes in bytes.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- Buckets: state.options.requestSizeBuckets,
- }, rpcLabels)
- state.options.registerer.MustRegister(reqSizes)
-
- respSizes := prometheus.NewHistogramVec(prometheus.HistogramOpts{
- Name: metricRPCResponseSize,
- Help: "Histogram of response sizes in bytes.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- Buckets: state.options.responseSizeBuckets,
- }, rpcLabels)
- state.options.registerer.MustRegister(respSizes)
-
- streamMsgSizes := prometheus.NewHistogramVec(prometheus.HistogramOpts{
- Name: metricRPCStreamMessageSize,
- Help: "Histogram of stream message sizes in bytes.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- Buckets: state.options.requestSizeBuckets,
- }, rpcNoCodeLabels)
- state.options.registerer.MustRegister(streamMsgSizes)
-
- streamResSizes := prometheus.NewHistogramVec(prometheus.HistogramOpts{
- Name: metricRPCStreamResponseSize,
- Help: "Histogram of stream response sizes in bytes.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- Buckets: state.options.responseSizeBuckets,
- }, rpcNoCodeLabels)
- state.options.registerer.MustRegister(streamResSizes)
-
- activeReqs := prometheus.NewGaugeVec(prometheus.GaugeOpts{
- Name: metricRPCActiveRequests,
- Help: "Gauge of active requests.",
- ConstLabels: prometheus.Labels{labelGoaService: state.svc},
- }, rpcNoCodeLabels)
- state.options.registerer.MustRegister(activeReqs)
-
- state.grpcMetrics = &grpcMetrics{
- Durations: durations,
- RequestSizes: reqSizes,
- ResponseSizes: respSizes,
- ActiveRequests: activeReqs,
- StreamMessageSizes: streamMsgSizes,
- StreamResultSizes: streamResSizes,
- }
-
- return state.grpcMetrics
-}
diff --git a/metrics/context_test.go b/metrics/context_test.go
deleted file mode 100644
index 577dfeda..00000000
--- a/metrics/context_test.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package metrics
-
-import (
- "context"
- "testing"
-)
-
-func TestContext(t *testing.T) {
- svc := "testsvc"
- optionList := []Option{WithDurationBuckets([]float64{0, 1, 2})}
- optionStruct := defaultOptions()
- optionStruct.durationBuckets = []float64{0, 1, 2}
- otherOptionList := []Option{WithRequestSizeBuckets([]float64{0, 1, 2})}
- ctx := Context(context.Background(), svc, optionList...)
- cases := []struct {
- name string
- ctx context.Context
- svc string
- options []Option
- want stateBag
- }{
- {"empty", context.Background(), "", nil, stateBag{options: defaultOptions()}},
- {"with options", context.Background(), "", optionList, stateBag{options: optionStruct}},
- {"with options and service", context.Background(), svc, optionList, stateBag{options: optionStruct, svc: svc}},
- {"with initialized context", ctx, "some-svc", otherOptionList, stateBag{options: optionStruct, svc: svc}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- ctx := Context(c.ctx, c.svc, c.options...)
- got := ctx.Value(stateBagKey).(*stateBag)
- if got.svc != c.want.svc {
- t.Errorf("unexpected svc: got %q, want %q", got.svc, c.want.svc)
- }
- if !sameOptions(got.options, c.want.options) {
- t.Errorf("unexpected options: got %v, want %v", got.options, c.want.options)
- }
- })
- }
-}
-
-func sameOptions(a, b *options) bool {
- if a == nil && b == nil {
- return true
- }
- if a == nil || b == nil {
- return false
- }
- if a.durationBuckets != nil && b.durationBuckets != nil {
- if len(a.durationBuckets) != len(b.durationBuckets) {
- return false
- }
- for i := range a.durationBuckets {
- if a.durationBuckets[i] != b.durationBuckets[i] {
- return false
- }
- }
- }
- if a.requestSizeBuckets != nil && b.requestSizeBuckets != nil {
- if len(a.requestSizeBuckets) != len(b.requestSizeBuckets) {
- return false
- }
- for i := range a.requestSizeBuckets {
- if a.requestSizeBuckets[i] != b.requestSizeBuckets[i] {
- return false
- }
- }
- }
- return true
-}
diff --git a/metrics/grpc.go b/metrics/grpc.go
deleted file mode 100644
index 32834439..00000000
--- a/metrics/grpc.go
+++ /dev/null
@@ -1,144 +0,0 @@
-package metrics
-
-import (
- "context"
- "strconv"
- "strings"
- "time"
-
- "github.com/prometheus/client_golang/prometheus"
- "google.golang.org/grpc"
- "google.golang.org/grpc/status"
- "google.golang.org/protobuf/proto"
-)
-
-type (
- // streamWrapper wraps a grpc.ServerStream with prometheus metrics.
- streamWrapper struct {
- grpc.ServerStream
- labels prometheus.Labels
- reqSizes, respSizes *prometheus.HistogramVec
- }
-)
-
-// UnaryServerInterceptor creates a gRPC unary server interceptor that metricss the
-// requests. The context must have been initialized with metrics.Context. The
-// returned interceptor adds the following metrics:
-//
-// - `grpc.server.duration`: Histogram of request durations in milliseconds.
-// - `grpc.server.active_requests`: UpDownCounter of active requests.
-// - `grpc.server.request.size`: Histogram of request sizes in bytes.
-// - `grpc.server.response.size`: Histogram of response sizes in bytes.
-//
-// All the metrics have the following labels:
-//
-// - `goa.method`: The method name as specified in the Goa design.
-// - `goa.service`: The service name as specified in the Goa design.
-// - `rpc.system`: A stream identifying the remoting system (e.g. `grpc`).
-// - `rpc.service`: Name of RPC service.
-// - `rpc.method`: Name of RPC method.
-// - `rpc.status_code`: The response status code.
-//
-// Errors collecting or serving metrics are logged to the logger in the context
-// if any.
-func UnaryServerInterceptor(ctx context.Context) grpc.UnaryServerInterceptor {
- b := ctx.Value(stateBagKey)
- if b == nil {
- panic("initialize context with Context first")
- }
- metrics := b.(*stateBag).GRPCMetrics()
-
- return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
- service, method := parseGRPCFullMethodName(info.FullMethod)
- labels := prometheus.Labels{labelRPCMethod: method, labelRPCService: service}
- metrics.ActiveRequests.With(labels).Add(1)
- defer metrics.ActiveRequests.With(labels).Sub(1)
-
- now := time.Now()
- resp, err := handler(ctx, req)
-
- st, _ := status.FromError(err)
- labels[labelRPCStatusCode] = strconv.Itoa(int(st.Code()))
- metrics.Durations.With(labels).Observe(float64(timeSince(now)) / float64(time.Millisecond))
- if msg, ok := req.(proto.Message); ok {
- metrics.RequestSizes.With(labels).Observe(float64(proto.Size(msg)))
- }
- if msg, ok := resp.(proto.Message); ok {
- metrics.ResponseSizes.With(labels).Observe(float64(proto.Size(msg)))
- }
-
- return resp, err
- }
-}
-
-// StreamServerInterceptor creates a gRPC stream server interceptor that metricss the
-// requests. The context must have been initialized with Context. The returned
-// interceptor adds the following metrics:
-//
-// - `grpc.server.active_requests`: UpDownCounter of active requests.
-// - `grpc.server.request.size`: Histogram of request sizes in bytes.
-// - `grpc.server.response.size`: Histogram of response sizes in bytes.
-//
-// All the metrics have the following labels:
-//
-// - `goa.method`: The method name as specified in the Goa design.
-// - `goa.service`: The service name as specified in the Goa design.
-// - `rpc.system`: A stream identifying the remoting system (e.g. `grpc`).
-// - `rpc.service`: Name of RPC service.
-// - `rpc.method`: Name of RPC method.
-// - `rpc.status_code`: The response status code.
-//
-// Errors collecting or serving metrics are logged to the logger in the context
-// if any.
-func StreamServerInterceptor(ctx context.Context) grpc.StreamServerInterceptor {
- b := ctx.Value(stateBagKey)
- if b == nil {
- panic("metrics not found in context, initialize context with Context first")
- }
- metrics := b.(*stateBag).GRPCMetrics()
-
- return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
- service, method := parseGRPCFullMethodName(info.FullMethod)
- labels := prometheus.Labels{labelRPCMethod: method, labelRPCService: service}
- metrics.ActiveRequests.With(labels).Add(1)
- defer metrics.ActiveRequests.With(labels).Sub(1)
-
- now := time.Now()
- wrapper := streamWrapper{stream, labels, metrics.StreamMessageSizes, metrics.StreamResultSizes}
- err := handler(srv, &wrapper)
-
- st, _ := status.FromError(err)
- labels[labelRPCStatusCode] = strconv.Itoa(int(st.Code()))
-
- metrics.Durations.With(labels).Observe(float64(timeSince(now)) / float64(time.Millisecond))
-
- return err
- }
-}
-
-func (s *streamWrapper) RecvMsg(m interface{}) error {
- if err := s.ServerStream.RecvMsg(m); err != nil {
- return err
- }
- if msg, ok := m.(proto.Message); ok {
- s.reqSizes.With(s.labels).Observe(float64(proto.Size(msg)))
- }
- return nil
-}
-
-func (s *streamWrapper) SendMsg(m interface{}) error {
- if msg, ok := m.(proto.Message); ok {
- s.respSizes.With(s.labels).Observe(float64(proto.Size(msg)))
- }
- return s.ServerStream.SendMsg(m)
-}
-
-func parseGRPCFullMethodName(fullMethodName string) (serviceName, methodName string) {
- if idx := strings.LastIndex(fullMethodName, "."); idx >= 0 {
- fullMethodName = fullMethodName[idx+1:]
- }
- if idx := strings.LastIndex(fullMethodName, "/"); idx > 0 {
- return fullMethodName[:idx], fullMethodName[idx+1:]
- }
- return fullMethodName, ""
-}
diff --git a/metrics/grpc_test.go b/metrics/grpc_test.go
deleted file mode 100644
index 5e7b97c8..00000000
--- a/metrics/grpc_test.go
+++ /dev/null
@@ -1,347 +0,0 @@
-package metrics
-
-import (
- "context"
- "strings"
- "sync"
- "testing"
- "time"
-
- "goa.design/clue/internal/testsvc"
- "google.golang.org/grpc"
-)
-
-func TestUnaryServerInterceptorServerDuration(t *testing.T) {
- buckets := []float64{10, 110}
- cases := []struct {
- name string
- d time.Duration
- expectedBucketCounts []int
- }{
- {"fast", 1 * time.Millisecond, []int{1, 1}},
- {"slow", 100 * time.Millisecond, []int{0, 1}},
- {"very slow", 1000 * time.Millisecond, []int{0, 0}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- restore := timeSince
- defer func() { timeSince = restore }()
- timeSince = func(time.Time) time.Duration { return c.d }
-
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg), WithDurationBuckets(buckets))
- uinter := UnaryServerInterceptor(ctx)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.UnaryInterceptor(uinter)),
- testsvc.WithUnaryFunc(noopMethod()))
-
- _, err := cli.GRPCMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
-
- stop()
- reg.AssertHistogram(metricRPCDuration, rpcLabels, 1, c.expectedBucketCounts)
- })
- }
-}
-
-func TestUnaryServerInterceptorRequestSize(t *testing.T) {
- buckets := []float64{10, 110}
- cases := []struct {
- name string
- str string
- expectedBucketCounts []int
- }{
- {"small", "1", []int{1, 1}},
- {"large", strings.Repeat("1", 100), []int{0, 1}},
- {"very large", strings.Repeat("1", 1000), []int{0, 0}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg), WithRequestSizeBuckets(buckets))
- uinter := UnaryServerInterceptor(ctx)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.UnaryInterceptor(uinter)),
- testsvc.WithUnaryFunc(noopMethod()))
-
- _, err := cli.GRPCMethod(context.Background(), &testsvc.Fields{S: &c.str})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
-
- stop()
- reg.AssertHistogram(metricRPCRequestSize, rpcLabels, 1, c.expectedBucketCounts)
- })
- }
-}
-
-func TestUnaryServerInterceptorResponseSize(t *testing.T) {
- buckets := []float64{10, 110}
- cases := []struct {
- name string
- str string
- expectedBucketCounts []int
- }{
- {"small", "1", []int{1, 1}},
- {"large", strings.Repeat("1", 100), []int{0, 1}},
- {"very large", strings.Repeat("1", 1000), []int{0, 0}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg), WithResponseSizeBuckets(buckets))
- uinter := UnaryServerInterceptor(ctx)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.UnaryInterceptor(uinter)),
- testsvc.WithUnaryFunc(stringMethod(c.str)))
-
- _, err := cli.GRPCMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
-
- stop()
- reg.AssertHistogram(metricRPCResponseSize, rpcLabels, 1, c.expectedBucketCounts)
- })
- }
-}
-
-func TestUnaryServerInterceptorActiveRequests(t *testing.T) {
- cases := []struct {
- name string
- numReqs int
- }{
- {"one", 1},
- {"ten", 10},
- {"one thousand", 1000},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg))
- uinter := UnaryServerInterceptor(ctx)
- chstop := make(chan struct{})
- var running, done sync.WaitGroup
- running.Add(c.numReqs)
- done.Add(c.numReqs)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.UnaryInterceptor(uinter)),
- testsvc.WithUnaryFunc(waitMethod(&running, &done, chstop)))
-
- for i := 0; i < c.numReqs; i++ {
- go cli.GRPCMethod(context.Background(), &testsvc.Fields{}) // nolint: errcheck
- }
-
- running.Wait()
- reg.AssertGauge(metricRPCActiveRequests, rpcNoCodeLabels, c.numReqs)
- close(chstop)
- done.Wait()
- stop()
- })
- }
-}
-
-func TestStreamServerInterceptorServerDuration(t *testing.T) {
- buckets := []float64{10, 110}
- cases := []struct {
- name string
- d time.Duration
- expectedBucketCounts []int
- }{
- {"fast", 1 * time.Millisecond, []int{1, 1}},
- {"slow", 100 * time.Millisecond, []int{0, 1}},
- {"very slow", 1000 * time.Millisecond, []int{0, 0}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- restore := timeSince
- defer func() { timeSince = restore }()
- timeSince = func(time.Time) time.Duration { return c.d }
-
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg), WithDurationBuckets(buckets))
- sinter := StreamServerInterceptor(ctx)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.StreamInterceptor(sinter)),
- testsvc.WithStreamFunc(echoMethod()))
-
- stream, err := cli.GRPCStream(context.Background())
- if err != nil {
- t.Errorf("unexpected stream error: %v", err)
- }
- if err := stream.Send(&testsvc.Fields{}); err != nil {
- t.Errorf("unexpected send error: %v", err)
- }
- if _, err := stream.Recv(); err != nil {
- t.Errorf("unexpected recv error: %v", err)
- }
-
- stop()
- reg.AssertHistogram(metricRPCDuration, rpcLabels, 1, c.expectedBucketCounts)
- })
- }
-}
-
-func TestStreamServerInterceptorMessageSize(t *testing.T) {
- buckets := []float64{10, 110}
- cases := []struct {
- name string
- str string
- expectedBucketCounts []int
- }{
- {"small", "1", []int{1, 1}},
- {"large", strings.Repeat("1", 100), []int{0, 1}},
- {"very large", strings.Repeat("1", 1000), []int{0, 0}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg), WithRequestSizeBuckets(buckets))
- sinter := StreamServerInterceptor(ctx)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.StreamInterceptor(sinter)),
- testsvc.WithStreamFunc(echoMethod()))
-
- stream, err := cli.GRPCStream(context.Background())
- if err != nil {
- t.Errorf("unexpected stream error: %v", err)
- }
- if err := stream.Send(&testsvc.Fields{S: &c.str}); err != nil {
- t.Errorf("unexpected send error: %v", err)
- }
- if _, err := stream.Recv(); err != nil {
- t.Errorf("unexpected recv error: %v", err)
- }
-
- stop()
- reg.AssertHistogram(metricRPCStreamMessageSize, rpcNoCodeLabels, 1, c.expectedBucketCounts)
- })
- }
-}
-
-func TestStreamServerInterceptorResponseSize(t *testing.T) {
- buckets := []float64{10, 110}
- cases := []struct {
- name string
- str string
- expectedBucketCounts []int
- }{
- {"small", "1", []int{1, 1}},
- {"large", strings.Repeat("1", 100), []int{0, 1}},
- {"very large", strings.Repeat("1", 1000), []int{0, 0}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg), WithResponseSizeBuckets(buckets))
- sinter := StreamServerInterceptor(ctx)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.StreamInterceptor(sinter)),
- testsvc.WithStreamFunc(echoMethod()))
-
- stream, err := cli.GRPCStream(context.Background())
- if err != nil {
- t.Errorf("unexpected stream error: %v", err)
- }
- if err := stream.Send(&testsvc.Fields{S: &c.str}); err != nil {
- t.Errorf("unexpected send error: %v", err)
- }
- if _, err := stream.Recv(); err != nil {
- t.Errorf("unexpected recv error: %v", err)
- }
-
- stop()
- reg.AssertHistogram(metricRPCStreamResponseSize, rpcNoCodeLabels, 1, c.expectedBucketCounts)
- })
- }
-}
-
-func TestStreamServerInterceptorActiveRequests(t *testing.T) {
- cases := []struct {
- name string
- numReqs int
- }{
- {"one", 1},
- {"ten", 10},
- {"one hundred", 100},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg))
- sinter := StreamServerInterceptor(ctx)
- chstop := make(chan struct{})
- var running, done sync.WaitGroup
- running.Add(c.numReqs)
- done.Add(c.numReqs)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.StreamInterceptor(sinter)),
- testsvc.WithStreamFunc(recvWaitMethod(&running, &done, chstop)))
-
- for i := 0; i < c.numReqs; i++ {
- stream, err := cli.GRPCStream(context.Background())
- if err != nil {
- t.Errorf("unexpected stream error: %v", err)
- }
- go func() {
- if err := stream.Send(&testsvc.Fields{}); err != nil {
- t.Errorf("unexpected send error: %v", err)
- }
- }()
- }
- running.Wait()
- reg.AssertGauge(metricRPCActiveRequests, rpcNoCodeLabels, c.numReqs)
- close(chstop)
- done.Wait()
- stop()
- })
- }
-}
-
-func noopMethod() testsvc.UnaryFunc {
- return func(_ context.Context, _ *testsvc.Fields) (*testsvc.Fields, error) {
- return &testsvc.Fields{}, nil
- }
-}
-
-func stringMethod(str string) testsvc.UnaryFunc {
- return func(_ context.Context, _ *testsvc.Fields) (*testsvc.Fields, error) {
- return &testsvc.Fields{S: &str}, nil
- }
-}
-
-func waitMethod(running, done *sync.WaitGroup, stop chan struct{}) testsvc.UnaryFunc {
- return func(_ context.Context, _ *testsvc.Fields) (*testsvc.Fields, error) {
- running.Done()
- defer done.Done()
- <-stop
- return &testsvc.Fields{}, nil
- }
-}
-
-func echoMethod() testsvc.StreamFunc {
- return func(_ context.Context, stream testsvc.Stream) (err error) {
- f, err := stream.Recv()
- if err != nil {
- return err
- }
- if err := stream.Send(f); err != nil {
- return err
- }
- return stream.Close()
- }
-}
-
-func recvWaitMethod(running, done *sync.WaitGroup, stop chan struct{}) testsvc.StreamFunc {
- return func(_ context.Context, stream testsvc.Stream) (err error) {
- running.Done()
- defer done.Done()
- if _, err := stream.Recv(); err != nil {
- return err
- }
- <-stop
- return stream.Close()
- }
-}
diff --git a/metrics/handler.go b/metrics/handler.go
deleted file mode 100644
index 259f3d85..00000000
--- a/metrics/handler.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package metrics
-
-import (
- "context"
- "net/http"
-
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promhttp"
-)
-
-type (
- // handlerOption is a function that configures the handler.
- handlerOption func(*handlerOptions)
-
- handlerOptions struct {
- // registerer is the prometheus registerer.
- registerer prometheus.Registerer
- // gatherer is the prometheus gatherer.
- gatherer prometheus.Gatherer
- }
-)
-
-// Handler returns a HTTP handler that collect metrics and serves them using the
-// Prometheus export formats. It uses the context logger configured via
-// micro/log if any to log errors. By default Handler uses the default
-// prometheus registry to gather metrics and to register its own metrics. Use
-// options WithGatherer and WithHandlerRegisterer to override the default values.
-func Handler(ctx context.Context, opts ...handlerOption) http.Handler {
- options := defaultHandlerOptions()
- for _, o := range opts {
- o(options)
- }
- return promhttp.InstrumentMetricHandler(options.registerer, promhttp.HandlerFor(options.gatherer,
- promhttp.HandlerOpts{
- ErrorLog: logger{ctx},
- Registry: options.registerer,
- }))
-}
-
-// WithHandlerRegisterer returns an option that sets the prometheus registerer.
-func WithHandlerRegisterer(registerer prometheus.Registerer) handlerOption {
- return func(c *handlerOptions) {
- c.registerer = registerer
- }
-}
-
-// WithGatherer returns an option that sets the prometheus gatherer used to
-// collect the metrics.
-func WithGatherer(gatherer prometheus.Gatherer) handlerOption {
- return func(c *handlerOptions) {
- c.gatherer = gatherer
- }
-}
-
-// defaultHandlerOptions returns a new HandlerOption struct with default values.
-func defaultHandlerOptions() *handlerOptions {
- return &handlerOptions{
- registerer: prometheus.DefaultRegisterer,
- gatherer: prometheus.DefaultGatherer,
- }
-}
diff --git a/metrics/handler_test.go b/metrics/handler_test.go
deleted file mode 100644
index 9486b130..00000000
--- a/metrics/handler_test.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package metrics
-
-import (
- "bytes"
- "context"
- "errors"
- "io"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- dto "github.com/prometheus/client_model/go"
- "goa.design/clue/log"
-)
-
-func TestHandler(t *testing.T) {
- errTest := errors.New("test")
- cases := []struct {
- name string
- err error
- expectedLog string
- expectedResponse string
- }{
- {"no error", nil, "", "test_metric 1"},
- {"error", errTest, errTest.Error(), ""},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- var buf bytes.Buffer
- ctx := log.Context(context.Background(),
- log.WithOutput(&buf),
- log.WithFormat(func(e *log.Entry) []byte { return []byte(e.KeyVals[0].V.(string)) }))
- reg := NewTestRegistry(t)
- gat := &mockGatherer{err: c.err}
- req, _ := http.NewRequest("GET", "/metrics", nil)
- w := httptest.NewRecorder()
-
- Handler(ctx, WithGatherer(gat), WithHandlerRegisterer(reg)).ServeHTTP(w, req)
-
- resp := w.Result()
- body, _ := io.ReadAll(resp.Body)
- if c.err != nil {
- if !strings.Contains(string(body), c.err.Error()) {
- t.Errorf("expected error message %q, got %q", c.err.Error(), string(body))
- }
- if w.Code != http.StatusInternalServerError {
- t.Errorf("expected status code %d, got %d", http.StatusInternalServerError, w.Code)
- }
- if !strings.Contains(buf.String(), c.expectedLog) {
- t.Errorf("expected log %q, got %q", c.expectedLog, buf.String())
- }
- return
- }
- if buf.Len() > 0 {
- t.Errorf("unexpected log message %q", buf.String())
- }
- if !strings.Contains(string(body), c.expectedResponse) {
- t.Errorf("expected response %q, got %q", c.expectedResponse, string(body))
- }
- })
- }
-}
-
-type mockGatherer struct {
- err error
-}
-
-func (m *mockGatherer) Gather() ([]*dto.MetricFamily, error) {
- var (
- name = "test_metric"
- one float64 = 1
- typ = dto.MetricType_COUNTER
- )
- if m.err != nil {
- return nil, m.err
- }
- return []*dto.MetricFamily{{
- Name: &name,
- Type: &typ,
- Metric: []*dto.Metric{{Counter: &dto.Counter{Value: &one}}},
- }}, nil
-}
diff --git a/metrics/http.go b/metrics/http.go
deleted file mode 100644
index 725c25d4..00000000
--- a/metrics/http.go
+++ /dev/null
@@ -1,118 +0,0 @@
-package metrics
-
-import (
- "context"
- "io"
- "net/http"
- "strconv"
- "time"
-
- "github.com/prometheus/client_golang/prometheus"
- "goa.design/goa/v3/http/middleware"
-)
-
-type (
- // lengthReader is a wrapper around an io.ReadCloser that keeps track of how
- // much data has been read.
- lengthReader struct {
- Source io.ReadCloser
- ctx context.Context
- }
-)
-
-// Be kind to tests
-var timeSince = time.Since
-
-// HTTP returns a middlware that metricss requests. The context must have
-// been initialized with Context. HTTP collects the following metrics:
-//
-// - `http.server.duration`: Histogram of request durations in milliseconds.
-// - `http.server.active_requests`: UpDownCounter of active requests.
-// - `http.server.request.size`: Histogram of request sizes in bytes.
-// - `http.server.response.size`: Histogram of response sizes in bytes.
-//
-// All the metrics have the following labels:
-//
-// - `http.verb`: The HTTP verb (`GET`, `POST` etc.).
-// - `http.host`: The value of the HTTP host header.
-// - `http.path`: The HTTP path.
-// - `http.status_code`: The HTTP status code.
-//
-// Errors collecting or serving metrics are logged to the logger in the context
-// if any.
-func HTTP(ctx context.Context) func(http.Handler) http.Handler {
- b := ctx.Value(stateBagKey)
- if b == nil {
- panic("initialize context with Context first")
- }
- metrics := b.(*stateBag).HTTPMetrics()
- resolver := b.(*stateBag).options.resolver
-
- return func(h http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- var route string
- if resolver != nil {
- route = resolver(req)
- } else {
- route = req.URL.Path
- }
- labels := prometheus.Labels{
- labelHTTPVerb: req.Method,
- labelHTTPHost: req.Host,
- labelHTTPPath: route,
- }
- metrics.ActiveRequests.With(labels).Add(1)
- defer metrics.ActiveRequests.With(labels).Sub(1)
-
- now := time.Now()
- rw := middleware.CaptureResponse(w)
- ctx, body := newLengthReader(req.Body, req.Context())
- req.Body = body
- req = req.WithContext(ctx)
-
- h.ServeHTTP(rw, req)
-
- labels[labelHTTPStatusCode] = strconv.Itoa(rw.StatusCode)
-
- reqLength := req.Context().Value(ctxReqLen).(*int)
- metrics.Durations.With(labels).Observe(float64(timeSince(now).Milliseconds()))
- metrics.RequestSizes.With(labels).Observe(float64(*reqLength))
- metrics.ResponseSizes.With(labels).Observe(float64(rw.ContentLength))
- })
- }
-}
-
-// So we have to do a little dance to get the length of the request body. We
-// can't just simply wrap the body and sum up the length on each read because
-// otel sets its own wrapper which means we can't cast the request back after
-// the call to the next handler. We thus store the computed length in the
-// context instead.
-func newLengthReader(body io.ReadCloser, ctx context.Context) (context.Context, *lengthReader) {
- reqLen := 0
- ctx = context.WithValue(ctx, ctxReqLen, &reqLen)
- return ctx, &lengthReader{body, ctx}
-}
-
-func (r *lengthReader) Read(b []byte) (int, error) {
- n, err := r.Source.Read(b)
- l := r.ctx.Value(ctxReqLen).(*int)
- *l += n
-
- return n, err
-}
-
-func (r *lengthReader) Close() error {
- var buf [32]byte
- var n int
- var err error
- for err == nil {
- n, err = r.Source.Read(buf[:])
- l := r.ctx.Value(ctxReqLen).(*int)
- *l += n
- }
- closeerr := r.Source.Close()
- if err != nil && err != io.EOF {
- return err
- }
- return closeerr
-}
diff --git a/metrics/http_test.go b/metrics/http_test.go
deleted file mode 100644
index 21378d7f..00000000
--- a/metrics/http_test.go
+++ /dev/null
@@ -1,185 +0,0 @@
-package metrics
-
-import (
- "context"
- "io"
- "strings"
- "sync"
- "testing"
- "time"
-
- "goa.design/clue/internal/testsvc"
-)
-
-func TestHTTPServerDuration(t *testing.T) {
- buckets := []float64{10, 110}
- cases := []struct {
- name string
- d time.Duration
- expectedBucketCounts []int
- }{
- {"fast", 1 * time.Millisecond, []int{1, 1}},
- {"slow", 100 * time.Millisecond, []int{0, 1}},
- {"very slow", 1000 * time.Millisecond, []int{0, 0}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- restore := timeSince
- defer func() { timeSince = restore }()
- timeSince = func(time.Time) time.Duration { return c.d }
-
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg), WithDurationBuckets(buckets))
- middleware := HTTP(ctx)
- cli, stop := testsvc.SetupHTTP(t,
- testsvc.WithHTTPMiddleware(middleware),
- testsvc.WithHTTPFunc(noopMethod()))
- _, err := cli.HTTPMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
-
- stop()
- reg.AssertHistogram(metricHTTPDuration, httpLabels, 1, c.expectedBucketCounts)
- })
- }
-}
-
-func TestHTTPRequestSize(t *testing.T) {
- buckets := []float64{10, 110}
- cases := []struct {
- name string
- str string
- expectedBucketCounts []int
- }{
- {"small", "1", []int{1, 1}},
- {"large", strings.Repeat("1", 100), []int{0, 1}},
- {"very large", strings.Repeat("1", 1000), []int{0, 0}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg), WithRequestSizeBuckets(buckets))
- middleware := HTTP(ctx)
- cli, stop := testsvc.SetupHTTP(t,
- testsvc.WithHTTPMiddleware(middleware),
- testsvc.WithHTTPFunc(noopMethod()))
-
- _, err := cli.HTTPMethod(context.Background(), &testsvc.Fields{S: &c.str})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
-
- stop()
- reg.AssertHistogram(metricHTTPRequestSize, httpLabels, 1, c.expectedBucketCounts)
- })
- }
-}
-
-func TestHTTPResponseSize(t *testing.T) {
- buckets := []float64{10, 110}
- cases := []struct {
- name string
- str string
- expectedBucketCounts []int
- }{
- {"small", "1", []int{1, 1}},
- {"large", strings.Repeat("1", 100), []int{0, 1}},
- {"very large", strings.Repeat("1", 1000), []int{0, 0}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg), WithResponseSizeBuckets(buckets))
- middleware := HTTP(ctx)
- cli, stop := testsvc.SetupHTTP(t,
- testsvc.WithHTTPMiddleware(middleware),
- testsvc.WithHTTPFunc(stringMethod(c.str)))
-
- _, err := cli.HTTPMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
-
- stop()
- reg.AssertHistogram(metricHTTPResponseSize, httpLabels, 1, c.expectedBucketCounts)
- })
- }
-}
-
-func TestHTTPActiveRequests(t *testing.T) {
- cases := []struct {
- name string
- numReqs int
- }{
- {"one", 1},
- {"ten", 10},
- {"one hundred", 100},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- reg := NewTestRegistry(t)
- ctx := Context(context.Background(), "testsvc", WithRegisterer(reg))
- middleware := HTTP(ctx)
- chstop := make(chan struct{})
- var running, done sync.WaitGroup
- running.Add(c.numReqs)
- done.Add(c.numReqs)
- cli, stop := testsvc.SetupHTTP(t,
- testsvc.WithHTTPMiddleware(middleware),
- testsvc.WithHTTPFunc(waitMethod(&running, &done, chstop)))
-
- for i := 0; i < c.numReqs; i++ {
- go func() {
- _, err := cli.HTTPMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- }()
- }
-
- running.Wait()
- reg.AssertGauge(metricHTTPActiveRequests, httpActiveRequestsLabels, c.numReqs)
- close(chstop)
- done.Wait()
- stop()
- })
- }
-}
-
-func TestLengthReader(t *testing.T) {
- cases := []struct {
- name string
- str string
- expectedSize int
- }{
- {"empty", "", 0},
- {"one", "1", 1},
- {"ten", strings.Repeat("1", 10), 10},
- {"one hundred", strings.Repeat("1", 100), 100},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- r := strings.NewReader(c.str)
- ctx, lr := newLengthReader(io.NopCloser(r), context.Background())
- n, err := lr.Read(make([]byte, 100))
- if err != nil && err != io.EOF {
- t.Errorf("unexpected error: %v", err)
- }
- if n != c.expectedSize {
- t.Errorf("expected %d bytes, got %d", c.expectedSize, n)
- }
- length := ctx.Value(ctxReqLen)
- if length == nil {
- t.Fatal("expected length to be set in context")
- }
- if *(length.(*int)) != c.expectedSize {
- t.Errorf("expected %d bytes, got %d", c.expectedSize, *(length.(*int)))
- }
- err = lr.Close()
- if err != nil {
- t.Errorf("unexpected close error: %v", err)
- }
- })
- }
-}
diff --git a/metrics/logger.go b/metrics/logger.go
deleted file mode 100644
index af716eb3..00000000
--- a/metrics/logger.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package metrics
-
-import (
- "context"
- "fmt"
-
- "goa.design/clue/log"
-)
-
-// clue/log to prometheus logger adapter.
-type logger struct {
- context.Context
-}
-
-// Implements the promhttp.Logger interface.
-func (l logger) Println(v ...interface{}) {
- msg := fmt.Sprintln(v...)
- log.Printf(l, msg)
-}
diff --git a/metrics/logger_test.go b/metrics/logger_test.go
deleted file mode 100644
index 40e75cc4..00000000
--- a/metrics/logger_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package metrics
-
-import (
- "bytes"
- "context"
- "testing"
-
- "goa.design/clue/log"
-)
-
-func TestLogger(t *testing.T) {
- data := struct {
- foo string
- bar int
- }{"foo", 1}
- cases := []struct {
- name string
- vals []interface{}
- expected string
- }{
- {"empty", []interface{}{}, "\n"},
- {"one", []interface{}{"one"}, "one\n"},
- {"many", []interface{}{"one", 2, data}, "one 2 {foo 1}\n"},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- var buf bytes.Buffer
- ctx := log.Context(context.Background(),
- log.WithOutput(&buf),
- log.WithFormat(func(e *log.Entry) []byte { return []byte(e.KeyVals[0].V.(string)) }))
- l := logger{ctx}
- l.Println(c.vals...)
- if buf.String() != c.expected {
- t.Errorf("expected %q, got %q", c.expected, buf.String())
- }
- })
- }
-}
diff --git a/metrics/options.go b/metrics/options.go
deleted file mode 100644
index 3aee2417..00000000
--- a/metrics/options.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package metrics
-
-import (
- "net/http"
-
- "github.com/prometheus/client_golang/prometheus"
-)
-
-type (
- // Option is a function that configures the metricsation.
- Option func(*options)
-
- // RouteResolver is a function that resolves the route of a request used
- // to label metrics. Using a route resolver makes it possible to label
- // all routes matching a pattern with the same label. As an example
- // services using the github.com/go-chi/chi/v5 muxer can use
- // chi.RouteContext(r.Context()).RoutePattern().
- RouteResolver func(r *http.Request) string
-
- // options contains the configuration for the metrics.
- options struct {
- // durationBuckets is the buckets for the request duration histogram.
- durationBuckets []float64
- // requestSizeBuckets is the buckets for the request size histogram.
- requestSizeBuckets []float64
- // responseSizeBuckets is the buckets for the response size histogram.
- responseSizeBuckets []float64
- // Prometheus registerer
- registerer prometheus.Registerer
- // RouteResolver is used to label metrics.
- resolver RouteResolver
- }
-)
-
-var (
- DefaultDurationBuckets = []float64{10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000}
- DefaultRequestSizeBuckets = []float64{10, 100, 500, 1000, 5000, 10000, 50000, 100000, 1000000, 10000000}
- DefaultResponseSizeBuckets = []float64{10, 100, 500, 1000, 5000, 10000, 50000, 100000, 1000000, 10000000}
-)
-
-// defaultOptions returns a new options struct with default values.
-func defaultOptions() *options {
- return &options{
- durationBuckets: DefaultDurationBuckets,
- requestSizeBuckets: DefaultRequestSizeBuckets,
- responseSizeBuckets: DefaultResponseSizeBuckets,
- registerer: prometheus.DefaultRegisterer,
- }
-}
-
-// WithRouteResolver returns an option that sets the route resolver used to
-// label metrics. The default uses the request path.
-func WithRouteResolver(resolver RouteResolver) Option {
- return func(o *options) {
- o.resolver = resolver
- }
-}
-
-// WithDurationBuckets returns an option that sets the duration buckets for the
-// request duration histogram.
-func WithDurationBuckets(buckets []float64) Option {
- return func(c *options) {
- c.durationBuckets = buckets
- }
-}
-
-// WithRequestSizeBuckets returns an option that sets the request size buckets
-// for the request size histogram.
-func WithRequestSizeBuckets(buckets []float64) Option {
- return func(c *options) {
- c.requestSizeBuckets = buckets
- }
-}
-
-// WithResponseSizeBuckets returns an option that sets the response size buckets
-// for the response size histogram.
-func WithResponseSizeBuckets(buckets []float64) Option {
- return func(c *options) {
- c.responseSizeBuckets = buckets
- }
-}
-
-// WithRegisterer returns an option that sets the prometheus registerer.
-func WithRegisterer(registerer prometheus.Registerer) Option {
- return func(c *options) {
- c.registerer = registerer
- }
-}
diff --git a/metrics/options_test.go b/metrics/options_test.go
deleted file mode 100644
index 8322f3d1..00000000
--- a/metrics/options_test.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package metrics
-
-import (
- "fmt"
- "net/http"
- "testing"
-
- "github.com/prometheus/client_golang/prometheus"
-)
-
-func TestOptions(t *testing.T) {
- var (
- durationBuckets = []float64{1}
- requestSizeBuckets = []float64{1}
- responseSizeBuckets = []float64{1}
- registerer = NewTestRegistry(t)
- resolver = func(_ *http.Request) string { return "test" }
- )
- options := defaultOptions()
- assertOptions(t, options, DefaultDurationBuckets, DefaultRequestSizeBuckets, DefaultResponseSizeBuckets, prometheus.DefaultRegisterer, nil)
-
- WithDurationBuckets(durationBuckets)(options)
- assertOptions(t, options, durationBuckets, DefaultRequestSizeBuckets, DefaultResponseSizeBuckets, prometheus.DefaultRegisterer, nil)
-
- WithRequestSizeBuckets(requestSizeBuckets)(options)
- assertOptions(t, options, durationBuckets, requestSizeBuckets, DefaultResponseSizeBuckets, prometheus.DefaultRegisterer, nil)
-
- WithResponseSizeBuckets(responseSizeBuckets)(options)
- assertOptions(t, options, durationBuckets, requestSizeBuckets, responseSizeBuckets, prometheus.DefaultRegisterer, nil)
-
- WithRegisterer(registerer)(options)
- assertOptions(t, options, durationBuckets, requestSizeBuckets, responseSizeBuckets, registerer, nil)
-
- WithRouteResolver(resolver)(options)
- assertOptions(t, options, durationBuckets, requestSizeBuckets, responseSizeBuckets, registerer, resolver)
-}
-
-func assertOptions(t *testing.T, options *options, durationBuckets []float64, requestSizeBuckets []float64, responseSizeBuckets []float64, registerer prometheus.Registerer, resolver RouteResolver) {
- if !equal(options.durationBuckets, durationBuckets) {
- t.Errorf("got %v, expected %v", options.durationBuckets, durationBuckets)
- }
- if !equal(options.requestSizeBuckets, requestSizeBuckets) {
- t.Errorf("got %v, expected %v", options.requestSizeBuckets, requestSizeBuckets)
- }
- if !equal(options.responseSizeBuckets, responseSizeBuckets) {
- t.Errorf("got %v, expected %v", options.responseSizeBuckets, responseSizeBuckets)
- }
- if options.registerer != registerer {
- t.Errorf("got %v, expected %v", options.registerer, registerer)
- }
- if options.resolver == nil {
- if resolver != nil {
- t.Errorf("got nil, expected %v", resolver)
- }
- return
- }
- if resolver == nil {
- t.Errorf("got %v, expected nil", options.resolver)
- return
- }
- if fmt.Sprintf("%+v", options.resolver) != fmt.Sprintf("%+v", resolver) {
- t.Errorf("got %v, expected %v", options.resolver, resolver)
- }
-}
-
-func equal(a, b []float64) bool {
- if len(a) != len(b) {
- return false
- }
- for i := range a {
- if a[i] != b[i] {
- return false
- }
- }
- return true
-}
diff --git a/metrics/prom_test.go b/metrics/prom_test.go
deleted file mode 100644
index 0bf5d2eb..00000000
--- a/metrics/prom_test.go
+++ /dev/null
@@ -1,113 +0,0 @@
-package metrics
-
-import (
- "testing"
-
- "github.com/prometheus/client_golang/prometheus"
- io_prometheus_client "github.com/prometheus/client_model/go"
-)
-
-type Registry struct {
- *prometheus.Registry
- t *testing.T
-}
-
-var _ prometheus.Registerer = (*Registry)(nil)
-
-func NewTestRegistry(t *testing.T) *Registry {
- return &Registry{prometheus.NewRegistry(), t}
-}
-
-// AssertGauge validates that the gauge with the given name and labels exists
-// and has the given value.
-func (r *Registry) AssertGauge(name string, labels []string, value int) {
- metric := r.findMetric(name, labels)
- if metric.Gauge == nil {
- r.t.Errorf("gauge %q with labels %v not found", name, labels)
- return
- }
- var val float64
- if metric.Gauge.Value != nil {
- val = *metric.Gauge.Value
- }
- if float64(value) != val {
- r.t.Errorf("gauge %q with labels %v has value %v, want %v", name, labels, val, value)
- }
-}
-
-// AssertHistogram validates that the histogram with the given name and given
-// labels exists and has the given sample count and cumulative counts for the
-// given buckets.
-func (r *Registry) AssertHistogram(name string, labels []string, sampleCount int, bucketCumulativeCount []int) {
- metric := r.findMetric(name, labels)
- if metric.Histogram == nil {
- r.t.Errorf("histogram %s with labels %v not found", name, labels)
- return
- }
- count := 0
- if metric.Histogram.SampleCount != nil {
- count = int(*metric.Histogram.SampleCount)
- }
- if count != sampleCount {
- r.t.Errorf("histogram %s with labels %v has sample count %d, want %d", name, labels, count, sampleCount)
- }
- if len(metric.Histogram.Bucket) != len(bucketCumulativeCount) {
- r.t.Fatalf("histogram %s with labels %v has %d buckets, want %d", name, labels, len(metric.Histogram.Bucket), len(bucketCumulativeCount))
- }
- for i, b := range metric.Histogram.Bucket {
- count := 0
- if b.CumulativeCount != nil {
- count = int(*b.CumulativeCount)
- }
- if count != bucketCumulativeCount[i] {
- r.t.Errorf("histogram %s with labels %v has bucket %d cumulative count %d, want %d", name, labels, i, count, bucketCumulativeCount[i])
- }
- }
-}
-
-// findMetric finds a metric in the registry with the given name and labels.
-func (r *Registry) findMetric(name string, labels []string) *io_prometheus_client.Metric {
- families, err := r.Gather()
- if err != nil {
- r.t.Errorf("failed to gather metrics: %v", err)
- }
- var metrics *io_prometheus_client.Metric
-loop:
- for _, family := range families {
- if family.Name == nil || *family.Name != name {
- continue
- }
- for _, m := range family.Metric {
- if !hasLabels(m.Label, labels) {
- continue
- }
- metrics = m
- break loop
- }
- }
- if metrics == nil {
- r.t.Fatalf("histogram %s with labels %v not found", name, labels)
- }
- return metrics
-}
-
-// hasLabels returns true if the given label names are a subset of the given
-// prometheus label names.
-func hasLabels(promlabels []*io_prometheus_client.LabelPair, names []string) bool {
- for _, lbl := range names {
- found := false
- for _, plbl := range promlabels {
- if plbl.Name == nil {
- continue
- }
- if *plbl.Name == lbl {
- found = true
- break
- }
- }
- if !found {
- return false
- }
- }
- return true
-}
diff --git a/trace/README.md b/trace/README.md
deleted file mode 100644
index a9b395bd..00000000
--- a/trace/README.md
+++ /dev/null
@@ -1,204 +0,0 @@
-# trace: Simple Request Tracing
-
-[![Build Status](https://github.com/goadesign/clue/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/goadesign/clue/actions?query=branch%3Amain+event%3Apush)
-[![Go Reference](https://pkg.go.dev/badge/goa.design/clue/trace.svg)](https://pkg.go.dev/goa.design/clue/trace)
-
-## Overview
-
-Package `trace` provides request tracing functionality that follows the
-[OpenTelemetry](https://opentelemetry.io/) specification. The package is
-designed to be used in conjunction with [Goa](https://goa.design/). In
-particular it is aware of the Goa RequestID
-[HTTP](https://github.com/goadesign/goa/blob/v3/http/middleware/requestid.go)
-and
-[gRPC](https://github.com/goadesign/goa/blob/v3/grpc/middleware/requestid.go)
-middlewares and adds both the Goa service name and current request ID to the
-span attributes.
-
-The package uses an adaptive sampler that is configured to sample at a given
-maximum number of request per seconds (2 per default). Using a time based rate
-rather than e.g. a fixed percentage rate allows the sampler to adapt to the load
-of the service.
-
-## Usage
-
-The following example shows how to use the package. It implements an
-illustrative `main` function for a fictional service `svc` implemented in the
-package `github.com/repo/services/svc`
-
-```go
-package main
-
-import (
- "context"
-
- "goa.design/clue/log"
- "goa.design/clue/trace"
- goahttp "goa.design/goa/v3/http"
-
- "github.com/repo/services/svc"
- httpsvrgen "github.com/repo/services/svc/gen/http/svc/server"
- grpcsvrgen "github.com/repo/services/svc/gen/grpc/svc/server"
- svcgen "github.com/repo/services/svc/gen/svc"
-)
-
-func main() {
- // Initialize the log context
- ctx := log.With(log.Context(context.Background()), "svc", svcgen.ServiceName)
- // Create the service (user code)
- svc := svc.New(ctx)
- // Wrap the service with Goa endpoints
- endpoints := svcgen.NewEndpoints(svc)
-
- // Create HTTP server
- mux := goahttp.NewMuxer()
- httpsvr := httpsvrgen.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil)
- httpsvrgen.Mount(mux, httpsvr)
-
- // ** Initialize context for tracing **
- conn, err := grpc.DialContext(ctx, "localhost:6831") // Remote span collector address
- if err != nil {
- log.Error(ctx, "unable to connect to span collector", "err", err)
- os.Exit(1)
- }
- ctx = trace.Context(ctx, svcgen.ServiceName, trace.WithGRPCExporter(conn))
-
- // ** Trace HTTP requests **
- handler := trace.HTTP(ctx)(mux)
-
- // Create gRPC server
- grpcsvr := grpcsvrgen.New(endpoints, nil)
-
- // ** Trace gRPC requests **
- u := trace.UnaryServerTrace(ctx)
- s := trace.StreamServerTrace(ctx)
- pbsvr := grpc.NewServer(grpc.UnaryInterceptor(u), grpc.StreamInterceptor(s))
-
- // ...
-}
-```
-
-### Making Requests to Downstream Dependencies
-
-For tracing to work appropriately all clients to downstream dependencies must be
-configured using the appropriate trace package function.
-
-For HTTP dependencies the trace package provides a `Client` function that can be
-used to configure a `http.RoundTripper` to trace all requests made through it.
-`Client` panics if the context hasn't be initialized with `trace.Context`.
-
-```go
-// Create a tracing HTTP client
-c := &http.Client{Transport: trace.Client(ctx, http.DefaultTransport)}
-```
-
-For gRPC dependencies the trace package provides the `UnaryClientTrace` and
-`StreamClientTrace` interceptors that can be used when making gRPC calls. These
-functions will create a span for the current request if it is traced. Example:
-
-```go
-// Create a tracing client for gRPC unary calls
-conn, err := grpc.Dial(url, grpc.WithUnaryInterceptor(UnaryClientTrace(ctx)))
-
-// Create a tracing client for gRPC stream calls
-conn, err := grpc.Dial(url, grpc.WithStreamInterceptor(StreamClientTrace(ctx)))
-```
-
-### Creating Additional Spans
-
-Once configured the trace package automatically creates spans for a sample of
-incoming requests. The function `IsTraced` can be used to determine if the
-current request is being traced.
-
-The trace package also provides a `StartSpan` function that can be used to
-create a new child span. The caller must also call `EndSpan` when the span is
-complete. Both functions do nothing if the current request is not being traced
-or the context has not been initialized with `trace.Context`.
-
-```go
-func (s *svc) DoSomething(ctx context.Context, req *svcgen.DoSomethingRequest) (*svcgen.DoSomethingResponse, error) {
- // ...
- // Create a child span to measure the time taken to run an intensive
- // operation.
- ctx = trace.StartSpan(ctx, "DoSomethingIntense")
- DoSomethingIntense(ctx)
- trace.EndSpan(ctx)
- // ...
-}
-```
-
-### Adding Attributes to Spans
-
-Attributes decorate spans and add contextual information to the trace. By default
-this package adds the following attributes to HTTP requests:
-
-* `http.scheme`: The HTTP scheme (`http` or `https`).
-* `http.host`: The host name of the request if available in the `Host` field.
-* `http.flavor`: The flavor of the request (`1.0`, `1.1` or `2`).
-* `http.method`: The HTTP method of the request.
-* `http.target`: The URI of the request.
-* `http.client_id`: The client IP present in the `X-Forwarded-For` header if any.
-* `http.user_agent`: The user agent of the request if any.
-* `http.request_content_length`: The length of the request body if any.
-* `enduser.id`: The request basic auth username if any.
-* `net.transport`: One of `ip_tcp`, `ip_udp`, `ip`, `unix` or `other`.
-* `net.peer.ip`, `net.peer.name`, `net.peer.port`: The IP address, port and name
- of the remote peer if available in the request `RemoteAddr` field.
-* `net.host.ip`, `net.host.name`, `net.host.port`: The IP address, port and name
- of the remote host if available in the request `Host` field, the request `Host`
- header or the request URL.
-
-and the following attributes to gRPC requests:
-
-* `rpc.system`: always set to `grpc`.
-* `rpc.service`: The gRPC service name.
-* `rpc.method`: The gRPC method name.
-* `net.peer.ip`, `net.peer.port`: The IP address and port of the remote peer.
-
-Service method logic can add attributes when creating new spans via the
-`WithAttributes` option. Custom attributes can also be added later on with
-`SetSpanAttributes`. `SetSpanAttributes` does nothing if the request is not
-traced or the context not initialized with `trace.Context`.
-
-```go
-// Create a child span with attributes
-ctx = trace.StartSpan(ctx, "DoSomething", trace.WithAttributes(
- "key1", "value1",
- "key2", "value2",
-))
-
-// Add a custom attribute to the current span
-trace.SetSpanAttributes(ctx, "custom_attribute", "value")
-```
-
-### Adding Events
-
-The `AddEvent` function makes it possible to attach events to a span. Events are
-useful to trace operations that are too fast to have their own span. For
-example, the completion of an asynchronous operation. Attributes can be added to
-the event to add contextual information. `AddEvent` does nothing if the request
-is not traced or the context not initialized with `trace.Context`.
-
-```go
-// Add an event to the current span
-trace.AddEvent(ctx, "operation completed", "operation_id", operationID, "status", status)
-```
-
-### Span Status And Error
-
-The `Succeed`, `Fail` and `RecordError` functions can be used to set the status
-and error of a span. The status indicates the success or failure of the
-operation. The error is used to record the error that occurred if any. Note
-that recording an error does not automatically change the status of the span.
-The functions do nothing if the request is not traced or the context not
-initialized with `trace.Context`. The default status of a completed span is
-success.
-
-```go
-// Set the status of the current span to success (default)
-trace.Succeed(ctx)
-
-// Record an error in the current span and set the status to failure
-trace.RecordError(ctx, err)
-trace.Fail(ctx, "operation failed")
-```
diff --git a/trace/context.go b/trace/context.go
deleted file mode 100644
index 018d2313..00000000
--- a/trace/context.go
+++ /dev/null
@@ -1,108 +0,0 @@
-package trace
-
-import (
- "context"
- "errors"
-
- "go.opentelemetry.io/otel/propagation"
- "go.opentelemetry.io/otel/sdk/resource"
- sdktrace "go.opentelemetry.io/otel/sdk/trace"
- semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
- "go.opentelemetry.io/otel/trace"
- "go.opentelemetry.io/otel/trace/noop"
-)
-
-type (
- // ctxKey is a private type used to store the tracer provider in the context.
- ctxKey int
-
- // stateBag tracks the provider, tracer and active span sequence for a request.
- stateBag struct {
- svc string
- provider trace.TracerProvider
- propagator propagation.TextMapPropagator
- tracer trace.Tracer
- spans []trace.Span
- }
-)
-
-const (
- // InstrumentationLibraryName is the name of the instrumentation library.
- InstrumentationLibraryName = "goa.design/clue"
-
- // AttributeRequestID is the name of the span attribute that contains the
- // request ID.
- AttributeRequestID = "request.id"
-)
-
-const (
- // stateKey is used to store the tracing state the context.
- stateKey ctxKey = iota + 1
-)
-
-// Context initializes the context so it can be used to create traces.
-func Context(ctx context.Context, svc string, opts ...TraceOption) (context.Context, error) {
- options := defaultOptions()
- for _, o := range opts {
- err := o(ctx, options)
- if err != nil {
- return nil, err
- }
- }
-
- if options.disabled {
- return withConfig(ctx, noop.NewTracerProvider(), options.propagator, svc), nil
- }
-
- if options.exporter == nil {
- return nil, errors.New("missing exporter, set one with the option 'trace.WithExporter'")
- }
-
- res := options.resource
- if res == nil {
- res = resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceNameKey.String(svc))
- }
-
- rootSampler := adaptiveSampler(options.maxSamplingRate, options.sampleSize)
- provider := sdktrace.NewTracerProvider(
- sdktrace.WithSampler(sdktrace.ParentBased(rootSampler, options.parentSamplerOptions...)),
- sdktrace.WithResource(res),
- sdktrace.WithBatcher(options.exporter),
- )
- return withConfig(ctx, provider, options.propagator, svc), nil
-}
-
-// IsTraced returns true if the current request is traced.
-func IsTraced(ctx context.Context) bool {
- span := trace.SpanFromContext(ctx)
- return span.IsRecording() && span.SpanContext().IsSampled()
-}
-
-// TraceProvider returns the underlying otel trace provider.
-func TraceProvider(ctx context.Context) trace.TracerProvider {
- sb := ctx.Value(stateKey).(*stateBag)
- return sb.provider
-}
-
-// withConfig stores the clue tracing config in the context.
-func withConfig(ctx context.Context, provider trace.TracerProvider, propagator propagation.TextMapPropagator, svc string) context.Context {
- return context.WithValue(ctx, stateKey, &stateBag{provider: provider, propagator: propagator, svc: svc})
-}
-
-// withTracing initializes the tracing context, ctx must have been initialized
-// with withProvider and the request must be traced by otel.
-func withTracing(traceCtx, ctx context.Context) context.Context {
- state := traceCtx.Value(stateKey).(*stateBag)
- svc := state.svc
- provider := state.provider
- propagator := state.propagator
- tracer := provider.Tracer(InstrumentationLibraryName)
- spans := []trace.Span{trace.SpanFromContext(ctx)}
- return context.WithValue(ctx, stateKey, &stateBag{
- svc: svc,
- provider: provider,
- propagator: propagator,
- tracer: tracer,
- spans: spans,
- })
-}
diff --git a/trace/context_test.go b/trace/context_test.go
deleted file mode 100644
index 0044799a..00000000
--- a/trace/context_test.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package trace
-
-import (
- "context"
- "testing"
-
- "go.opentelemetry.io/otel/propagation"
- "go.opentelemetry.io/otel/sdk/resource"
- sdktrace "go.opentelemetry.io/otel/sdk/trace"
- "go.opentelemetry.io/otel/sdk/trace/tracetest"
- "go.opentelemetry.io/otel/trace"
-)
-
-func testContext(provider trace.TracerProvider) context.Context {
- return withConfig(context.Background(), provider, propagation.TraceContext{}, "test")
-}
-
-func TestContext(t *testing.T) {
- // We don't want to test otel, keep it simple...
- exporter := tracetest.NewInMemoryExporter()
- ctx, err := Context(context.Background(), "test",
- WithMaxSamplingRate(3), WithSampleSize(20), WithExporter(exporter),
- WithResource(&resource.Resource{}), WithParentSamplerOptions(sdktrace.WithRemoteParentSampled(nil)),
- )
- if err != nil {
- t.Fatal(err)
- }
- s := ctx.Value(stateKey)
- if s == nil {
- t.Fatal("expected state in context")
- }
- st, ok := s.(*stateBag)
- if !ok {
- t.Fatalf("got %T, expected *stateBag", s)
- }
- if st.provider == nil {
- t.Error("expected provider in tracing context")
- }
-}
-
-func TestDisabled(t *testing.T) {
- ctx, err := Context(context.Background(), "test", WithDisabled())
- if err != nil {
- t.Fatal(err)
- }
- s := ctx.Value(stateKey)
- if s == nil {
- t.Fatal("expected state in context")
- }
- st, ok := s.(*stateBag)
- if !ok {
- t.Fatalf("got %T, expected *stateBag", s)
- }
- if st.provider == nil {
- t.Error("expected provider in tracing context")
- }
- _, span := st.provider.Tracer("test").Start(ctx, "test")
- if span.IsRecording() {
- t.Error("expected span to be disabled")
- }
-}
-
-func TestIsTraced(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- ctx, err := Context(context.Background(), "test",
- WithMaxSamplingRate(3), WithSampleSize(20), WithExporter(exporter))
- if err != nil {
- t.Fatal(err)
- }
- if IsTraced(ctx) {
- t.Error("expected not traced")
- }
- ctx, err = Context(ctx, "test", WithMaxSamplingRate(3), WithSampleSize(20), WithExporter(exporter))
- if err != nil {
- t.Fatal(err)
- }
- ctx = withTracing(ctx, context.Background())
- ctx = StartSpan(ctx, "test")
- if !IsTraced(ctx) {
- t.Error("expected traced")
- }
- EndSpan(ctx)
-}
diff --git a/trace/grpc.go b/trace/grpc.go
deleted file mode 100644
index 4717c7bd..00000000
--- a/trace/grpc.go
+++ /dev/null
@@ -1,188 +0,0 @@
-package trace
-
-import (
- "context"
-
- "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
- "go.opentelemetry.io/otel/attribute"
- "go.opentelemetry.io/otel/trace"
- "goa.design/goa/v3/middleware"
- "google.golang.org/grpc"
- "google.golang.org/grpc/stats"
-)
-
-// NewServerHandler creates a stats.Handler for a gRPC server that uses an
-// adaptive sampler for limiting the number of traced requests while
-// guraranteeing that a certain number of requests are traced.
-// It panics if the context has not been initialized with Context.
-//
-// Example:
-//
-// // Connect to remote trace collector.
-// conn, err := grpc.DialContext(ctx, collectorAddr)
-// if err != nil {
-// log.Error(ctx, err)
-// os.Exit(1)
-// }
-// // Initialize context for tracing
-// ctx := trace.Context(ctx, svcgen.ServiceName, trace.WithGRPCExporter(conn))
-// // Create stats handler
-// handler := trace.NewServerHandler(ctx)
-// // Create gRPC server
-// grpcServer := grpc.NewServer(grpc.StatsHandler(handler))
-func NewServerHandler(traceCtx context.Context) stats.Handler {
- state := traceCtx.Value(stateKey)
- if state == nil {
- panic(errContextMissing)
- }
- return otelgrpc.NewServerHandler(
- otelgrpc.WithTracerProvider(state.(*stateBag).provider),
- otelgrpc.WithPropagators(state.(*stateBag).propagator),
- )
-}
-
-// NewClientHandler creates a stats.Handler for a gRPC client.
-// It panics if the context has not been initialized with Context.
-func NewClientHandler(traceCtx context.Context) stats.Handler {
- state := traceCtx.Value(stateKey)
- if state == nil {
- panic(errContextMissing)
- }
- return otelgrpc.NewClientHandler(
- otelgrpc.WithTracerProvider(state.(*stateBag).provider),
- otelgrpc.WithPropagators(state.(*stateBag).propagator),
- )
-}
-
-// UnaryServerInterceptor returns an OpenTelemetry UnaryServerInterceptor. It
-// panics if the context has not been initialized with Context.
-// Deprecated: Use NewServerHandler instead.
-func UnaryServerInterceptor(traceCtx context.Context) grpc.UnaryServerInterceptor {
- state := traceCtx.Value(stateKey)
- if state == nil {
- panic(errContextMissing)
- }
- return func(
- ctx context.Context,
- req interface{},
- info *grpc.UnaryServerInfo,
- handler grpc.UnaryHandler,
- ) (interface{}, error) {
- handler = initTracingContextGRPCUnary(traceCtx, handler)
- handler = addRequestIDGRPCUnary(handler)
- ui := otelgrpc.UnaryServerInterceptor( //
- otelgrpc.WithTracerProvider(state.(*stateBag).provider),
- otelgrpc.WithPropagators(state.(*stateBag).propagator))
- return ui(ctx, req, info, handler)
- }
-}
-
-// StreamServerInterceptor returns an OpenTelemetry StreamServerInterceptor. It
-// panics if the context has not been initialized with Context.
-// Deprecated: Use NewServerHandler instead.
-func StreamServerInterceptor(traceCtx context.Context) grpc.StreamServerInterceptor {
- state := traceCtx.Value(stateKey)
- if state == nil {
- panic(errContextMissing)
- }
- return func(
- srv interface{},
- stream grpc.ServerStream,
- info *grpc.StreamServerInfo,
- handler grpc.StreamHandler,
- ) error {
- handler = initTracingContextGRPCStream(traceCtx, handler)
- handler = addRequestIDGRPCStream(handler)
- si := otelgrpc.StreamServerInterceptor(
- otelgrpc.WithTracerProvider(state.(*stateBag).provider),
- otelgrpc.WithPropagators(state.(*stateBag).propagator),
- )
- return si(srv, stream, info, handler)
- }
-}
-
-// UnaryClientInterceptor returns an OpenTelemetry UnaryClientInterceptor. It
-// panics if the context has not been initialized with Context.
-// Deprecated: Use NewClientHandler instead.
-func UnaryClientInterceptor(traceCtx context.Context) grpc.UnaryClientInterceptor {
- state := traceCtx.Value(stateKey)
- if state == nil {
- panic(errContextMissing)
- }
- return otelgrpc.UnaryClientInterceptor(
- otelgrpc.WithTracerProvider(state.(*stateBag).provider),
- otelgrpc.WithPropagators(state.(*stateBag).propagator))
-}
-
-// StreamClientInterceptor returns an OpenTelemetry StreamClientInterceptor. It
-// panics if the context has not been initialized with Context.
-// Deprecated: Use NewClientHandler instead.
-func StreamClientInterceptor(traceCtx context.Context) grpc.StreamClientInterceptor {
- state := traceCtx.Value(stateKey)
- if state == nil {
- panic(errContextMissing)
- }
- return otelgrpc.StreamClientInterceptor(
- otelgrpc.WithTracerProvider(state.(*stateBag).provider),
- otelgrpc.WithPropagators(state.(*stateBag).propagator))
-}
-
-// addRequestIDGRPCUnary is a middleware that adds the request ID to the current span
-// attributes.
-func addRequestIDGRPCUnary(h grpc.UnaryHandler) grpc.UnaryHandler {
- return func(ctx context.Context, req interface{}) (interface{}, error) {
- requestID := ctx.Value(middleware.RequestIDKey)
- if requestID == nil {
- return h(ctx, req)
- }
- span := trace.SpanFromContext(ctx)
- span.SetAttributes(attribute.String(AttributeRequestID, requestID.(string)))
- return h(ctx, req)
- }
-}
-
-// initTracingContextGRPCUnary is a unary interceptor that initializes the
-// tracing context.
-func initTracingContextGRPCUnary(traceCtx context.Context, h grpc.UnaryHandler) grpc.UnaryHandler {
- return func(ctx context.Context, req interface{}) (interface{}, error) {
- if IsTraced(ctx) {
- ctx = withTracing(traceCtx, ctx)
- }
- return h(ctx, req)
- }
-}
-
-// addRequestIDGRPCStream is a middleware that adds the request ID to the current span
-// attributes.
-func addRequestIDGRPCStream(h grpc.StreamHandler) grpc.StreamHandler {
- return func(srv interface{}, stream grpc.ServerStream) error {
- requestID := stream.Context().Value(middleware.RequestIDKey)
- if requestID == nil {
- return h(srv, stream)
- }
- span := trace.SpanFromContext(stream.Context())
- span.SetAttributes(attribute.String(AttributeRequestID, requestID.(string)))
- return h(srv, stream)
- }
-}
-
-// initTracingContextGRPCStream is a stream interceptor that initializes the
-// tracing context.
-func initTracingContextGRPCStream(traceCtx context.Context, h grpc.StreamHandler) grpc.StreamHandler {
- return func(srv interface{}, stream grpc.ServerStream) error {
- if IsTraced(stream.Context()) {
- ctx := withTracing(traceCtx, stream.Context())
- stream = &streamWithContext{ctx: ctx, ServerStream: stream}
- }
- return h(srv, stream)
- }
-}
-
-type streamWithContext struct {
- grpc.ServerStream
- ctx context.Context
-}
-
-func (s *streamWithContext) Context() context.Context {
- return s.ctx
-}
diff --git a/trace/grpc_test.go b/trace/grpc_test.go
deleted file mode 100644
index c1028d99..00000000
--- a/trace/grpc_test.go
+++ /dev/null
@@ -1,195 +0,0 @@
-package trace
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- sdktrace "go.opentelemetry.io/otel/sdk/trace"
- "go.opentelemetry.io/otel/sdk/trace/tracetest"
- "goa.design/clue/internal/testsvc"
- "goa.design/goa/v3/grpc/middleware"
- "google.golang.org/grpc"
-)
-
-func TestNewServerHandler(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- ctx := testContext(provider)
- handler := NewServerHandler(ctx)
- require.NotNil(t, handler)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.StatsHandler(handler)),
- testsvc.WithUnaryFunc(addEventUnaryMethod))
- _, err := cli.GRPCMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- stop()
- spans := exporter.GetSpans()
- require.Len(t, spans, 1)
- assert.Equal(t, "test.Test/GrpcMethod", spans[0].Name)
-}
-
-func TestNewClientHandler(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- ctx := testContext(provider)
- handler := NewClientHandler(ctx)
- require.NotNil(t, handler)
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithDialOptions(grpc.WithStatsHandler(handler)),
- testsvc.WithUnaryFunc(addEventUnaryMethod),
- )
- _, err := cli.GRPCMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- stop()
- spans := exporter.GetSpans()
- require.Len(t, spans, 1)
- assert.Equal(t, "test.Test/GrpcMethod", spans[0].Name)
-}
-
-func TestUnaryServerTrace(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- traceInterceptor := UnaryServerInterceptor(testContext(provider))
- requestIDInterceptor := middleware.UnaryRequestID()
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.ChainUnaryInterceptor(requestIDInterceptor, traceInterceptor)),
- testsvc.WithUnaryFunc(addEventUnaryMethod))
- _, err := cli.GRPCMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- stop()
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, want 1", len(spans))
- }
- found := false
- for _, att := range spans[0].Attributes {
- if att.Key == AttributeRequestID {
- found = true
- break
- }
- }
- if !found {
- t.Errorf("request ID not in span attributes")
- }
- events := spans[0].Events
- if len(events) != 1 {
- t.Fatalf("got %d events, want 1", len(events))
- }
- if events[0].Name != "unary method" {
- t.Errorf("unexpected event name: %s", events[0].Name)
- }
-}
-
-func TestUnaryServerTraceNoRequestID(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- traceInterceptor := UnaryServerInterceptor(testContext(provider))
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.UnaryInterceptor(traceInterceptor)),
- testsvc.WithUnaryFunc(addEventUnaryMethod))
- _, err := cli.GRPCMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- stop()
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, want 1", len(spans))
- }
-}
-
-func TestStreamServerTrace(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- traceInterceptor := StreamServerInterceptor(testContext(provider))
- requestIDInterceptor := middleware.StreamRequestID()
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.ChainStreamInterceptor(requestIDInterceptor, traceInterceptor)),
- testsvc.WithStreamFunc(echoMethod))
- stream, err := cli.GRPCStream(context.Background())
- if err != nil {
- t.Errorf("unexpected stream error: %v", err)
- }
- if err := stream.Send(&testsvc.Fields{}); err != nil {
- t.Errorf("unexpected send error: %v", err)
- }
- stream.Recv() // nolint: errcheck
- stop()
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, want 1", len(spans))
- }
- found := false
- for _, att := range spans[0].Attributes {
- if att.Key == AttributeRequestID {
- found = true
- break
- }
- }
- if !found {
- t.Errorf("request ID not in span attributes")
- }
-}
-
-func TestStreamServerTraceNoRequestID(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- traceInterceptor := StreamServerInterceptor(testContext(provider))
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithServerOptions(grpc.StreamInterceptor(traceInterceptor)),
- testsvc.WithStreamFunc(echoMethod))
- stream, err := cli.GRPCStream(context.Background())
- if err != nil {
- t.Errorf("unexpected stream error: %v", err)
- }
- if err := stream.Send(&testsvc.Fields{}); err != nil {
- t.Errorf("unexpected send error: %v", err)
- }
- stream.Recv() // nolint: errcheck
- stop()
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, want 1", len(spans))
- }
-}
-
-func TestUnaryClientTrace(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- cli, stop := testsvc.SetupGRPC(t,
- testsvc.WithDialOptions(grpc.WithUnaryInterceptor(UnaryClientInterceptor(testContext(provider)))),
- testsvc.WithUnaryFunc(addEventUnaryMethod))
- _, err := cli.GRPCMethod(context.Background(), &testsvc.Fields{})
- if err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- stop()
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, want 1", len(spans))
- }
-}
-
-func addEventUnaryMethod(ctx context.Context, _ *testsvc.Fields) (*testsvc.Fields, error) {
- AddEvent(ctx, "unary method")
- return &testsvc.Fields{}, nil
-}
-
-func echoMethod(_ context.Context, stream testsvc.Stream) (err error) {
- f, err := stream.Recv()
- if err != nil {
- return err
- }
- if err := stream.Send(f); err != nil {
- return err
- }
- return stream.Close()
-}
diff --git a/trace/http.go b/trace/http.go
deleted file mode 100644
index 3cace953..00000000
--- a/trace/http.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package trace
-
-import (
- "context"
- "net/http"
-
- "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
-)
-
-// Message printed by panic when using a method with a non-initialized context.
-const errContextMissing = "context not initialized for tracing, use trace.Context to set it up"
-
-// HTTP returns a tracing middleware that uses an adaptive sampler for limiting
-// the number of traced requests while guraranteeing that a certain number of
-// requests are traced.
-// HTTP panics if the context hasn't been initialized with Context.
-//
-// Example:
-//
-// // Connect to remote trace collector.
-// conn, err := grpc.DialContext(ctx, collectorAddr)
-// if err != nil {
-// log.Error(ctx, err)
-// os.Exit(1)
-// }
-// // Initialize context for tracing
-// ctx := trace.Context(ctx, svcgen.ServiceName, trace.WithGRPCExporter(conn))
-// // Mount middleware
-// handler := trace.HTTP(ctx)(mux)
-func HTTP(ctx context.Context) func(http.Handler) http.Handler {
- s := ctx.Value(stateKey)
- if s == nil {
- panic(errContextMissing)
- }
- return func(h http.Handler) http.Handler {
- return otelhttp.NewHandler(h, s.(*stateBag).svc,
- otelhttp.WithTracerProvider(s.(*stateBag).provider),
- otelhttp.WithPropagators(s.(*stateBag).propagator))
- }
-}
-
-// Client returns a http.RoundTripper that wraps t and creates spans for each
-// request. It panics if the context hasn't been initialized with Context.
-//
-// Example:
-//
-// // Connect to remote trace collector.
-// conn, err := grpc.DialContext(ctx, collectorAddr)
-// if err != nil {
-// log.Error(ctx, err)
-// os.Exit(1)
-// }
-// // Initialize context for tracing
-// ctx := trace.Context(ctx, svcgen.ServiceName, trace.WithGRPCExporter(conn))
-// // Create client
-// cli := &http.Client{
-// Transport: trace.Client(ctx, http.DefaultTransport),
-// }
-// // Use client
-// resp, err := cli.Get("http://example.com")
-func Client(ctx context.Context, t http.RoundTripper, opts ...otelhttp.Option) http.RoundTripper {
- s := ctx.Value(stateKey)
- if s == nil {
- panic(errContextMissing)
- }
- opts = append(opts,
- otelhttp.WithTracerProvider(s.(*stateBag).provider),
- otelhttp.WithPropagators(s.(*stateBag).propagator))
- return otelhttp.NewTransport(t, opts...)
-}
diff --git a/trace/http_test.go b/trace/http_test.go
deleted file mode 100644
index d59e7694..00000000
--- a/trace/http_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package trace
-
-import (
- "context"
- "net/http"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
- sdktrace "go.opentelemetry.io/otel/sdk/trace"
- "go.opentelemetry.io/otel/sdk/trace/tracetest"
-
- "goa.design/clue/internal/testsvc"
-)
-
-func TestHTTP(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- ctx := testContext(provider)
- cli, stop := testsvc.SetupHTTP(t,
- testsvc.WithHTTPMiddleware(HTTP(ctx)),
- testsvc.WithHTTPFunc(addEventUnaryMethod))
- _, err := cli.HTTPMethod(context.Background(), &testsvc.Fields{})
- assert.NoError(t, err)
- stop()
- spans := exporter.GetSpans()
- require.Len(t, spans, 1)
- assert.Equal(t, "test", spans[0].Name)
-}
-
-func TestClient(t *testing.T) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- ctx := testContext(provider)
- c := http.Client{Transport: Client(ctx, http.DefaultTransport)}
- otelt, ok := c.Transport.(*otelhttp.Transport)
- assert.True(t, ok, "got %T, want %T", c.Transport, otelt)
-}
diff --git a/trace/log.go b/trace/log.go
deleted file mode 100644
index 76820c74..00000000
--- a/trace/log.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package trace
-
-import (
- "context"
-
- "goa.design/clue/log"
-)
-
-// Log is a log key/value pair generator function that can be used to log trace
-// and span IDs. Example:
-//
-// ctx := log.Context(ctx, WithFunc(trace.Log))
-// log.Printf(ctx, "message")
-//
-// Output: traceID= spanID= message
-func Log(ctx context.Context) (kvs []log.KV) {
- if id := TraceID(ctx); id != "" {
- kvs = append(kvs, log.KV{K: log.TraceIDKey, V: id})
- }
- if id := SpanID(ctx); id != "" {
- kvs = append(kvs, log.KV{K: log.SpanIDKey, V: id})
- }
- return
-}
diff --git a/trace/log_test.go b/trace/log_test.go
deleted file mode 100644
index a239aa0a..00000000
--- a/trace/log_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package trace
-
-import (
- "context"
- "testing"
-
- "goa.design/clue/log"
-)
-
-func TestLog(t *testing.T) {
- ctx, _ := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
- ctx = StartSpan(ctx, "span")
- kvs := Log(ctx)
- if len(kvs) != 2 {
- t.Fatalf("got %d kvs, expected 2", len(kvs))
- }
- if kvs[0].K != log.TraceIDKey {
- t.Errorf("got kvs[0].K %q, expected %q", kvs[0].K, log.TraceIDKey)
- }
- if kvs[0].V != TraceID(ctx) {
- t.Errorf("got kvs[0].V %q, expected %q", kvs[0].V, TraceID(ctx))
- }
- if kvs[1].K != log.SpanIDKey {
- t.Errorf("got kvs[1].K %q, expected %q", kvs[1].K, log.SpanIDKey)
- }
- if kvs[1].V != SpanID(ctx) {
- t.Errorf("got kvs[1].V %q, expected %q", kvs[1].V, SpanID(ctx))
- }
-}
diff --git a/trace/options.go b/trace/options.go
deleted file mode 100644
index 9678da9f..00000000
--- a/trace/options.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package trace
-
-import (
- "context"
-
- "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
- "go.opentelemetry.io/otel/propagation"
- "go.opentelemetry.io/otel/sdk/resource"
- sdktrace "go.opentelemetry.io/otel/sdk/trace"
- "google.golang.org/grpc"
-)
-
-type (
- options struct {
- maxSamplingRate int
- sampleSize int
- exporter sdktrace.SpanExporter
- propagator propagation.TextMapPropagator
- parentSamplerOptions []sdktrace.ParentBasedSamplerOption
- resource *resource.Resource
- disabled bool
- }
-
- // TraceOption is a function that configures a provider.
- TraceOption func(ctx context.Context, opts *options) error
-)
-
-// defaultOptions returns the default sampler options.
-func defaultOptions() *options {
- return &options{
- maxSamplingRate: 2,
- sampleSize: 10,
- propagator: propagation.TraceContext{},
- }
-}
-
-// WithMaxSamplingRate sets the maximum sampling rate in requests per second.
-func WithMaxSamplingRate(rate int) TraceOption {
- return func(_ context.Context, opts *options) error {
- opts.maxSamplingRate = rate
- return nil
- }
-}
-
-// WithSampleSize sets the number of requests between two adjustments of the
-// sampling rate.
-func WithSampleSize(size int) TraceOption {
- return func(_ context.Context, opts *options) error {
- opts.sampleSize = size
- return nil
- }
-}
-
-// WithDisabled disables tracing, not for use in production.
-func WithDisabled() TraceOption {
- return func(_ context.Context, opts *options) error {
- opts.disabled = true
- return nil
- }
-}
-
-// WithExporter sets the exporter to use.
-func WithExporter(exporter sdktrace.SpanExporter) TraceOption {
- return func(_ context.Context, opts *options) error {
- opts.exporter = exporter
- return nil
- }
-}
-
-// WithParentSamplerOptions to set the options for sdktrace.ParentBased sampler.
-func WithParentSamplerOptions(samplerOptions ...sdktrace.ParentBasedSamplerOption) TraceOption {
- return func(_ context.Context, opts *options) error {
- opts.parentSamplerOptions = samplerOptions
- return nil
- }
-}
-
-// WithResource sets the underlying opentelemetry resource.
-func WithResource(res *resource.Resource) TraceOption {
- return func(_ context.Context, opts *options) error {
- opts.resource = res
- return nil
- }
-}
-
-// WithPropagator sets the trace propagator.
-func WithPropagator(propagator propagation.TextMapPropagator) TraceOption {
- return func(_ context.Context, opts *options) error {
- opts.propagator = propagator
- return nil
- }
-}
-
-// WithGRPCExporter sets the connection to the span exporter.
-func WithGRPCExporter(conn *grpc.ClientConn) TraceOption {
- return func(ctx context.Context, opts *options) error {
- exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
- if err != nil {
- return err
- }
- opts.exporter = exporter
- return nil
- }
-}
diff --git a/trace/options_test.go b/trace/options_test.go
deleted file mode 100644
index 1a053c0e..00000000
--- a/trace/options_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package trace
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "go.opentelemetry.io/otel/sdk/resource"
- sdktrace "go.opentelemetry.io/otel/sdk/trace"
- "go.opentelemetry.io/otel/sdk/trace/tracetest"
-)
-
-func TestOptions(t *testing.T) {
- ctx := context.Background()
-
- options := defaultOptions()
- if options.maxSamplingRate != 2 {
- t.Errorf("got %d, want 2", options.maxSamplingRate)
- }
- if options.sampleSize != 10 {
- t.Errorf("got %d, want 10", options.sampleSize)
- }
- assert.NoError(t, WithMaxSamplingRate(3)(ctx, options))
- if options.maxSamplingRate != 3 {
- t.Errorf("got %d sampling rate, want 3", options.maxSamplingRate)
- }
- assert.NoError(t, WithSampleSize(20)(ctx, options))
- if options.sampleSize != 20 {
- t.Errorf("got %d sample size, want 20", options.sampleSize)
- }
- assert.NoError(t, WithDisabled()(ctx, options))
- if !options.disabled {
- t.Error("expected disabled to be true")
- }
- assert.NoError(t, WithExporter(tracetest.NewInMemoryExporter())(ctx, options))
- if options.exporter == nil {
- t.Error("got nil exporter, want non-nil")
- }
- assert.NoError(t, WithResource(&resource.Resource{})(ctx, options))
- if options.resource == nil {
- t.Error("got nil resource, want non-nil")
- }
- assert.NoError(t, WithParentSamplerOptions(sdktrace.WithRemoteParentSampled(nil))(ctx, options))
- if total := len(options.parentSamplerOptions); total != 1 {
- t.Errorf("got %d parent sampler options, expected 1", total)
- }
-}
diff --git a/trace/sampler_test.go b/trace/sampler_test.go
deleted file mode 100644
index 6ac5c5cd..00000000
--- a/trace/sampler_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package trace
-
-import (
- "testing"
-
- sdktrace "go.opentelemetry.io/otel/sdk/trace"
-)
-
-func TestAdaptiveSampler(t *testing.T) {
- // We don't need to test Goa, keep it simple...
- s := adaptiveSampler(2, 10)
- expected := "Adaptive{maxSamplingRate:2,sampleSize:10}"
- if s.Description() != expected {
- t.Fatalf("got description %q, expected %q", s.Description(), expected)
- }
- res := s.ShouldSample(sdktrace.SamplingParameters{})
- if res.Decision != sdktrace.RecordAndSample {
- t.Error("expected sampling")
- }
-
- s2 := adaptiveSampler(1, 2)
- expected = "Adaptive{maxSamplingRate:1,sampleSize:2}"
- if s2.Description() != expected {
- t.Fatalf("got description %q, expected %q", s2.Description(), expected)
- }
- res2 := s2.ShouldSample(sdktrace.SamplingParameters{})
- res3 := s2.ShouldSample(sdktrace.SamplingParameters{})
- if res2.Decision != sdktrace.RecordAndSample {
- t.Error("expected sampling")
- }
- if res3.Decision != sdktrace.Drop {
- t.Error("expected no sampling")
- }
-}
diff --git a/trace/span.go b/trace/span.go
deleted file mode 100644
index 8404ad37..00000000
--- a/trace/span.go
+++ /dev/null
@@ -1,194 +0,0 @@
-package trace
-
-import (
- "context"
-
- "go.opentelemetry.io/otel/attribute"
- "go.opentelemetry.io/otel/codes"
- "go.opentelemetry.io/otel/trace"
-)
-
-// StartSpan starts a new span with the given name and attributes and stores it
-// in the returned context if the request is traced, does nothing otherwise.
-// keyvals must be a list of alternating keys and values.
-func StartSpan(ctx context.Context, name string, keyvals ...string) context.Context {
- s := ctx.Value(stateKey)
- if s == nil {
- return ctx
- }
- tracer := s.(*stateBag).tracer
- if tracer == nil {
- return ctx
- }
- ctx, span := tracer.Start(ctx, name, trace.WithAttributes(toKeyVal(keyvals)...))
- setActiveSpans(ctx, append(activeSpans(ctx), span))
- return ctx
-}
-
-// End ends the current span if any.
-func EndSpan(ctx context.Context) {
- spans := activeSpans(ctx)
- if len(spans) == 0 {
- return
- }
- spans[len(spans)-1].End()
- setActiveSpans(ctx, spans[:len(spans)-1])
-}
-
-// StartTrace starts a new trace and initializes the context with it. In general
-// traces should be managed by HTTP middlewares and gRPC interceptors created
-// via the HTTP, UnaryServerInterceptor and StreamServerInterceptor methods.
-// This function is intended to be used by code running outside of network
-// requests for example workers that initiate request threads. The context must
-// be initialized with Context. EndTrace must be called by the client in the
-// same goroutine. Not calling EndTrace may cause resource leaks.
-func StartTrace(ctx context.Context, name string, keyvals ...string) context.Context {
- return createSpan(ctx, trace.SpanKindClient, name, keyvals...)
-}
-
-// ContinueRemoteTrace initializes the tracing context with the given remote
-// trace ID and starts a new server span. See StartTrace for usage.
-func ContinueRemoteTrace(ctx context.Context, name string, traceID [16]byte, keyvals ...string) context.Context {
- sc := trace.NewSpanContext(trace.SpanContextConfig{
- TraceID: traceID,
- Remote: true,
- })
- return createSpan(trace.ContextWithRemoteSpanContext(ctx, sc), trace.SpanKindServer, name, keyvals...)
-}
-
-// EndTrace ends the last trace started by StartTrace.
-func EndTrace(ctx context.Context) {
- for _, span := range activeSpans(ctx) {
- span.End()
- }
- setActiveSpans(ctx, nil)
-}
-
-// SetSpanAttributes adds the given attributes to the current span if any.
-// keyvals must be a list of alternating keys and values. It overwrites any
-// existing attributes with the same key.
-func SetSpanAttributes(ctx context.Context, keyvals ...string) {
- span := activeSpan(ctx)
- if span == nil {
- return
- }
- span.SetAttributes(toKeyVal(keyvals)...)
-}
-
-// AddEvent records an event with the given name and attributes in the current
-// span if any.
-func AddEvent(ctx context.Context, name string, keyvals ...string) {
- span := activeSpan(ctx)
- if span == nil {
- return
- }
- kvs := toKeyVal(keyvals)
- span.AddEvent(name, trace.WithAttributes(kvs...))
-}
-
-// Succeed sets the status of the current span to success if any.
-func Succeed(ctx context.Context) {
- span := activeSpan(ctx)
- if span == nil {
- return
- }
- span.SetStatus(codes.Ok, "")
-}
-
-// Fail sets the status of the current span to failed and attaches the failure
-// message.
-func Fail(ctx context.Context, msg string) {
- span := activeSpan(ctx)
- if span == nil {
- return
- }
- span.SetStatus(codes.Error, msg)
-}
-
-// RecordError records err as an exception span event for the current span if
-// any. An additional call to SetStatus is required if the Status of the Span
-// should be set to Error, as this method does not change the Span status.
-func RecordError(ctx context.Context, err error) {
- span := activeSpan(ctx)
- if span == nil {
- return
- }
- span.RecordError(err)
-}
-
-// TraceID returns the trace ID of the current span if any, empty string otherwise.
-func TraceID(ctx context.Context) string {
- span := activeSpan(ctx)
- if span == nil {
- return ""
- }
- return span.SpanContext().TraceID().String()
-}
-
-// SpanID returns the span ID of the current span if any, empty string otherwise.
-func SpanID(ctx context.Context) string {
- span := activeSpan(ctx)
- if span == nil {
- return ""
- }
- return span.SpanContext().SpanID().String()
-}
-
-// createSpan creates a new span with the given name and attributes.
-func createSpan(ctx context.Context, kind trace.SpanKind, name string, keyvals ...string) context.Context {
- s := ctx.Value(stateKey)
- if s == nil {
- return ctx
- }
- bag := s.(*stateBag)
- tracer := bag.tracer
- if tracer == nil {
- tracer = bag.provider.Tracer(InstrumentationLibraryName)
- bag.tracer = tracer
- }
- ctx, span := tracer.Start(
- ctx,
- name,
- trace.WithSpanKind(kind),
- trace.WithAttributes(toKeyVal(keyvals)...),
- )
- setActiveSpans(ctx, []trace.Span{span})
- return ctx
-}
-
-// activeSpans returns the active spans of the tracing state.
-func activeSpans(ctx context.Context) []trace.Span {
- s := ctx.Value(stateKey)
- if s == nil {
- return nil
- }
- return s.(*stateBag).spans
-}
-
-// setActiveSpans updates the active spans of the tracing state.
-func setActiveSpans(ctx context.Context, spans []trace.Span) {
- s := ctx.Value(stateKey)
- if s == nil {
- return
- }
- s.(*stateBag).spans = spans
-}
-
-func activeSpan(ctx context.Context) trace.Span {
- spans := activeSpans(ctx)
- if len(spans) == 0 {
- return nil
- }
- return spans[len(spans)-1]
-}
-
-func toKeyVal(kvs []string) []attribute.KeyValue {
- if len(kvs)%2 != 0 {
- kvs = append(kvs, "")
- }
- keyvals := make([]attribute.KeyValue, len(kvs)/2)
- for i := 0; i < len(kvs); i += 2 {
- keyvals[i/2] = attribute.String(kvs[i], kvs[i+1])
- }
- return keyvals
-}
diff --git a/trace/span_test.go b/trace/span_test.go
deleted file mode 100644
index 3b0528dc..00000000
--- a/trace/span_test.go
+++ /dev/null
@@ -1,383 +0,0 @@
-package trace
-
-import (
- "context"
- "errors"
- "testing"
-
- "go.opentelemetry.io/otel/attribute"
- "go.opentelemetry.io/otel/codes"
- "go.opentelemetry.io/otel/propagation"
- sdktrace "go.opentelemetry.io/otel/sdk/trace"
- "go.opentelemetry.io/otel/sdk/trace/tracetest"
- "go.opentelemetry.io/otel/trace"
-)
-
-func newTestTracingContext() (context.Context, *tracetest.InMemoryExporter) {
- exporter := tracetest.NewInMemoryExporter()
- provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
- ctx := withConfig(context.Background(), provider, propagation.TraceContext{}, "test")
- return ctx, exporter
-}
-
-func TestStartEndSpan(t *testing.T) {
- // Make sure StartSpan and EndSpan do not panic
- StartSpan(context.Background(), "noop")
- EndSpan(context.Background())
-
- // Setup
- ctx, exporter := newTestTracingContext()
-
- // Create span
- ctx = withTracing(ctx, context.Background())
- ctx = StartSpan(ctx, "span")
- if !trace.SpanFromContext(ctx).SpanContext().IsValid() {
- t.Error("expected valid span")
- }
- if !trace.SpanFromContext(ctx).IsRecording() {
- t.Error("expected recording span")
- }
- if !trace.SpanFromContext(ctx).SpanContext().IsSampled() {
- t.Error("expected sampled span")
- }
- EndSpan(ctx)
-
- // Verify
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, expected 1", len(spans))
- }
- if spans[0].Name != "span" {
- t.Errorf("got span name %q, expected %q", spans[0].Name, "span")
- }
- var zeroID trace.TraceID
- if spans[0].Parent.TraceID() != zeroID {
- t.Errorf("got parent traceID %s, expected %s", spans[0].Parent.TraceID(), zeroID)
- }
-}
-
-func TestChildSpan(t *testing.T) {
- // Setup
- ctx, exporter := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
-
- // Create spans
- ctx = StartSpan(ctx, "parent")
- ctx = StartSpan(ctx, "child")
- EndSpan(ctx)
- EndSpan(ctx)
-
- // Verify )
- spans := exporter.GetSpans() // returns spans in reverse order
- if len(spans) != 2 {
- t.Fatalf("got %d spans, expected 2", len(spans))
- }
- if spans[1].Name != "parent" {
- t.Errorf("got span name %q, expected %q", spans[1].Name, "parent")
- }
- var zeroID trace.TraceID
- if spans[1].Parent.TraceID() != zeroID {
- t.Errorf("got parent span %v, expected %v", spans[1].Parent.TraceID(), zeroID)
- }
- if spans[0].Name != "child" {
- t.Errorf("got span name %q, expected %q", spans[0].Name, "child")
- }
- if spans[0].Parent.TraceID() != spans[1].SpanContext.TraceID() {
- t.Errorf("got parent span %v, expected %v", spans[0].Parent, spans[1])
- }
-}
-
-func TestStartEndTrace(t *testing.T) {
- // Setup
- ctx, exporter := newTestTracingContext()
-
- // Create trace
- ctx = StartTrace(ctx, "trace")
- EndTrace(ctx)
-
- // Verify
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, expected 1", len(spans))
- }
- if spans[0].Name != "trace" {
- t.Errorf("got span name %q, expected %q", spans[0].Name, "span")
- }
- var zeroID trace.TraceID
- if spans[0].Parent.TraceID() != zeroID {
- t.Errorf("got parent traceID %s, expected %s", spans[0].Parent.TraceID(), zeroID)
- }
-}
-
-func TestContinueRemoteTrace(t *testing.T) {
- // Setup
- ctx, exporter := newTestTracingContext()
-
- // Create span and set status
- traceID := [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}
- ctx = ContinueRemoteTrace(ctx, "trace", traceID)
- EndTrace(ctx)
-
- // Verify
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, expected 1", len(spans))
- }
- if spans[0].Name != "trace" {
- t.Errorf("got span name %q, expected %q", spans[0].Name, "span")
- }
- if spans[0].Parent.TraceID() != traceID {
- t.Errorf("got parent traceID %s, expected %s", spans[0].Parent.TraceID(), traceID)
- }
-}
-
-func TestSetSpanAttributesNoContext(t *testing.T) {
- SetSpanAttributes(context.Background(), "key", "value")
-}
-
-func TestSetSpanAttributes(t *testing.T) {
- cases := []struct {
- name string
- keyvals []string
- expected map[string]string
- }{
- {"nil", nil, nil},
- {"empty", []string{}, nil},
- {"one", []string{"key", "value"}, map[string]string{"key": "value"}},
- {"invalid", []string{"key", "value", "key2"}, map[string]string{"key": "value", "key2": ""}},
- {"override", []string{"key", "value", "key", "value2"}, map[string]string{"key": "value2"}},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- // Setup
- ctx, exporter := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
-
- // Create span and set attributes
- ctx = StartSpan(ctx, "span")
- SetSpanAttributes(ctx, c.keyvals...)
- EndSpan(ctx)
-
- // Verify
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, expected 1", len(spans))
- }
- if len(spans[0].Attributes) != len(c.expected) {
- t.Errorf("got %d attributes, expected %d", len(spans[0].Attributes), len(c.expected))
- }
- for k, v := range c.expected {
- found := false
- for _, att := range spans[0].Attributes {
- if att.Key == attribute.Key(k) && att.Value.AsString() == v {
- found = true
- break
- }
- }
- if !found {
- t.Errorf("attribute %s not found", k)
- }
- }
- })
- }
-}
-
-func TestAddEventNoContext(t *testing.T) {
- AddEvent(context.Background(), "event")
-}
-
-func TestAddEvent(t *testing.T) {
- // Setup
- ctx, exporter := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
-
- // Create span and add event
- ctx = StartSpan(ctx, "span")
- AddEvent(ctx, "event", "key", "value")
- EndSpan(ctx)
-
- // Verify
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, expected 1", len(spans))
- }
- if len(spans[0].Events) != 1 {
- t.Errorf("got %d events, expected 1", len(spans[0].Events))
- }
- if spans[0].Events[0].Name != "event" {
- t.Errorf("got event name %q, expected %q", spans[0].Events[0].Name, "event")
- }
- if len(spans[0].Events[0].Attributes) != 1 {
- t.Fatalf("got %d attributes, expected 1", len(spans[0].Events[0].Attributes))
- }
- if spans[0].Events[0].Attributes[0].Key != "key" {
- t.Errorf("got event attribute key %q, expected %q", spans[0].Events[0].Attributes[0].Key, "key")
- }
- if spans[0].Events[0].Attributes[0].Value.AsString() != "value" {
- t.Errorf("got event attribute value %q, expected %q", spans[0].Events[0].Attributes[0].Value.AsString(), "value")
- }
-}
-
-func TestFailNoContext(t *testing.T) {
- Fail(context.Background(), "desc")
-}
-
-func TestFail(t *testing.T) {
- // Setup
- ctx, exporter := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
-
- // Create span and set status
- ctx = StartSpan(ctx, "span")
- Fail(ctx, "desc")
- EndSpan(ctx)
-
- // Verify
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, expected 1", len(spans))
- }
- if spans[0].Status.Code != codes.Error {
- t.Errorf("got status code %d, expected %d", spans[0].Status.Code, codes.Error)
- }
- if spans[0].Status.Description != "desc" {
- t.Errorf("got status description %q, expected %q", spans[0].Status.Description, "desc")
- }
-}
-
-func TestSucceedNoContext(t *testing.T) {
- Succeed(context.Background())
-}
-
-func TestSucceed(t *testing.T) {
- // Setup
- ctx, exporter := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
-
- // Create span and set status
- ctx = StartSpan(ctx, "span")
- Fail(ctx, "desc")
- Succeed(ctx)
- EndSpan(ctx)
-
- // Verify
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, expected 1", len(spans))
- }
- if spans[0].Status.Code != codes.Ok {
- t.Errorf("got status code %d, expected %d", spans[0].Status.Code, codes.Ok)
- }
-}
-
-func TestRecordErrorNoContext(t *testing.T) {
- RecordError(context.Background(), errors.New("err"))
-}
-
-func TestRecordError(t *testing.T) {
- // Setup
- ctx, exporter := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
-
- // Create span and add event
- ctx = StartSpan(ctx, "span")
- recordedErr := errors.New("recorded error")
- RecordError(ctx, recordedErr)
- EndSpan(ctx)
-
- // Verify
- spans := exporter.GetSpans()
- if len(spans) != 1 {
- t.Fatalf("got %d spans, expected 1", len(spans))
- }
- if len(spans[0].Events) != 1 {
- t.Errorf("got %d events, expected 1", len(spans[0].Events))
- }
- if spans[0].Events[0].Name != "exception" {
- t.Errorf("got event name %q, expected %q", spans[0].Events[0].Name, "exception")
- }
- if spans[0].Events[0].Attributes[0].Key != "exception.type" {
- t.Errorf("got event attribute key %q, expected %q", spans[0].Events[0].Attributes[0].Key, "exception.type")
- }
- if spans[0].Events[0].Attributes[0].Value.AsString() != "*errors.errorString" {
- t.Errorf("got event attribute value %q, expected %q", spans[0].Events[0].Attributes[0].Value.AsString(), "*errors.errorString")
- }
-}
-
-func TestTraceIDNoContext(t *testing.T) {
- tid := TraceID(context.Background())
- if tid != "" {
- t.Errorf("got trace ID %q, expected empty", tid)
- }
-}
-
-func TestTraceID(t *testing.T) {
- // Setup
- ctx, _ := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
-
- // Create span and validate
- ctx = StartSpan(ctx, "span")
- if trace.SpanFromContext(ctx).SpanContext().TraceID().String() != TraceID(ctx) {
- t.Errorf("got trace ID %q, expected %q", trace.SpanFromContext(ctx).SpanContext().TraceID().String(), TraceID(ctx))
- }
- EndSpan(ctx)
-}
-
-func TestSpanIDNoContext(t *testing.T) {
- sid := SpanID(context.Background())
- if sid != "" {
- t.Errorf("got span ID %q, expected empty", sid)
- }
-}
-
-func TestSpanID(t *testing.T) {
- // Setup
- ctx, _ := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
-
- // Create span and add event
- ctx = StartSpan(ctx, "span")
- if trace.SpanFromContext(ctx).SpanContext().SpanID().String() != SpanID(ctx) {
- t.Errorf("got span ID %q, expected %q", trace.SpanFromContext(ctx).SpanContext().SpanID().String(), SpanID(ctx))
- }
- EndSpan(ctx)
-}
-
-func TestSetActiveSpansNoContext(t *testing.T) {
- setActiveSpans(context.Background(), nil)
-}
-
-func TestActiveSpans(t *testing.T) {
- // Setup
- ctx, _ := newTestTracingContext()
- ctx = withTracing(ctx, context.Background())
-
- // Create out-of-state span
- ctx, span := ctx.Value(stateKey).(*stateBag).tracer.Start(ctx, "span")
-
- // Make sure it's not in state
- spans := activeSpans(ctx)
- if len(spans) != 1 {
- t.Fatalf("got %d active spans, expected 1", len(spans))
- }
-
- // Manually add it to state
- setActiveSpans(ctx, []trace.Span{span})
- spans = activeSpans(ctx)
- if len(spans) != 1 {
- t.Fatalf("got %d active spans, expected 1", len(spans))
- }
- if spans[0] != span {
- t.Errorf("got active span %#v, expected %#v", spans[0], span)
- }
-
- // End span
- EndSpan(ctx)
-
- // Make sure it's not in state anymore
- spans = activeSpans(ctx)
- if len(spans) != 0 {
- t.Fatalf("got %d active spans, expected 0", len(spans))
- }
-}