diff --git a/.gitignore b/.gitignore index e8d626ce..86518fce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ .idea .DS_Store coverage.out +*__debug_* +config.json diff --git a/client.go b/client.go index ef15ce0a..957a8f99 100644 --- a/client.go +++ b/client.go @@ -15,6 +15,11 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/vektah/gqlparser/v2/ast" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.21.0" + "go.opentelemetry.io/otel/trace" ) // GraphQLClient is a GraphQL client. @@ -22,6 +27,8 @@ type GraphQLClient struct { HTTPClient *http.Client MaxResponseSize int64 UserAgent string + + tracer trace.Tracer } // ClientOpt is a function used to set a GraphQL client option @@ -33,10 +40,15 @@ func NewClient(opts ...ClientOpt) *GraphQLClient { } func NewClientWithPlugins(plugins []Plugin, opts ...ClientOpt) *GraphQLClient { + var transport http.RoundTripper = http.DefaultTransport + transport = otelhttp.NewTransport(transport) + c := &GraphQLClient{ HTTPClient: &http.Client{ - Timeout: 5 * time.Second, + Transport: transport, + Timeout: 5 * time.Second, }, + tracer: otel.GetTracerProvider().Tracer(instrumentationName), MaxResponseSize: 1024 * 1024, } @@ -51,13 +63,18 @@ func NewClientWithPlugins(plugins []Plugin, opts ...ClientOpt) *GraphQLClient { } func NewClientWithoutKeepAlive(opts ...ClientOpt) *GraphQLClient { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.DisableKeepAlives = true + var defaultTransport = http.DefaultTransport.(*http.Transport).Clone() + defaultTransport.DisableKeepAlives = true + + var transport http.RoundTripper = defaultTransport + transport = otelhttp.NewTransport(transport) + c := &GraphQLClient{ HTTPClient: &http.Client{ Timeout: 5 * time.Second, Transport: transport, }, + tracer: otel.GetTracerProvider().Tracer(instrumentationName), MaxResponseSize: 1024 * 1024, } @@ -93,15 +110,35 @@ func WithUserAgent(userAgent string) ClientOpt { // Request executes a GraphQL request. func (c *GraphQLClient) Request(ctx context.Context, url string, request *Request, out interface{}) error { + ctx, span := c.tracer.Start(ctx, "GraphQL Request", + trace.WithSpanKind(trace.SpanKindInternal), + trace.WithAttributes( + semconv.GraphqlOperationTypeKey.String(string(request.OperationType)), + semconv.GraphqlOperationName(request.OperationName), + semconv.GraphqlDocument(request.Query), + ), + ) + + defer span.End() + + traceErr := func(err error) error { + if err == nil { + return err + } + + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return err + } + var buf bytes.Buffer - err := json.NewEncoder(&buf).Encode(request) - if err != nil { - return fmt.Errorf("unable to encode request body: %w", err) + if err := json.NewEncoder(&buf).Encode(request); err != nil { + return traceErr(fmt.Errorf("unable to encode request body: %w", err)) } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) if err != nil { - return fmt.Errorf("unable to create request: %w", err) + return traceErr(fmt.Errorf("unable to create request: %w", err)) } if request.Headers != nil { @@ -121,8 +158,13 @@ func (c *GraphQLClient) Request(ctx context.Context, url string, request *Reques promServiceTimeoutErrorCounter.With(prometheus.Labels{ "service": url, }).Inc() + + // Return raw timeout error to allow caller to handle it since a + // downstream caller may want to retry, and they will have to jump + // through hoops to detect this error otherwise. + return traceErr(err) } - return fmt.Errorf("error during request: %w", err) + return traceErr(fmt.Errorf("error during request: %w", err)) } defer res.Body.Close() @@ -140,18 +182,17 @@ func (c *GraphQLClient) Request(ctx context.Context, url string, request *Reques Data: out, } - err = json.NewDecoder(&limitReader).Decode(&graphqlResponse) - if err != nil { + if err = json.NewDecoder(&limitReader).Decode(&graphqlResponse); err != nil { if errors.Is(err, io.ErrUnexpectedEOF) { if limitReader.N == 0 { - return fmt.Errorf("response exceeded maximum size of %d bytes", maxResponseSize) + return traceErr(fmt.Errorf("response exceeded maximum size of %d bytes", maxResponseSize)) } } - return fmt.Errorf("error decoding response: %w", err) + return traceErr(fmt.Errorf("error decoding response: %w", err)) } if len(graphqlResponse.Errors) > 0 { - return graphqlResponse.Errors + return traceErr(graphqlResponse.Errors) } return nil @@ -159,6 +200,7 @@ func (c *GraphQLClient) Request(ctx context.Context, url string, request *Reques // Request is a GraphQL request. type Request struct { + OperationType string `json:"operationType,omitempty"` Query string `json:"query"` OperationName string `json:"operationName,omitempty"` Variables map[string]interface{} `json:"variables,omitempty"` @@ -177,7 +219,20 @@ func (r *Request) WithHeaders(headers http.Header) *Request { return r } +func (r *Request) WithOperationType(operation string) *Request { + op := strings.ToLower(operation) + switch op { + case "query", "mutation", "subscription": + r.OperationType = op + default: + r.OperationType = "query" + } + + return r +} + func (r *Request) WithOperationName(operationName string) *Request { + r.OperationName = operationName return r } diff --git a/config.go b/config.go index 6f6510ec..078f1eb7 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package bramble import ( + "context" "encoding/json" "fmt" "net/http" @@ -11,6 +12,8 @@ import ( "github.com/fsnotify/fsnotify" log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" ) var Version = "dev" @@ -47,8 +50,9 @@ type Config struct { LogLevel log.Level `json:"loglevel"` PollInterval string `json:"poll-interval"` PollIntervalDuration time.Duration - MaxRequestsPerQuery int64 `json:"max-requests-per-query"` - MaxServiceResponseSize int64 `json:"max-service-response-size"` + MaxRequestsPerQuery int64 `json:"max-requests-per-query"` + MaxServiceResponseSize int64 `json:"max-service-response-size"` + Telemetry TelemetryConfig `json:"telemetry"` Plugins []PluginConfig // Config extensions that can be shared among plugins Extensions map[string]json.RawMessage @@ -58,6 +62,7 @@ type Config struct { plugins []Plugin executableSchema *ExecutableSchema watcher *fsnotify.Watcher + tracer trace.Tracer configFiles []string linkedFiles []string } @@ -250,20 +255,34 @@ func (c *Config) Watch() { continue } - err := c.Load() - if err != nil { + if err := c.reload(); err != nil { log.WithError(err).Error("error reloading config") } - log.WithField("services", c.Services).Info("config file updated") - err = c.executableSchema.UpdateServiceList(c.Services) - if err != nil { - log.WithError(err).Error("error updating services") - } - log.WithField("services", c.Services).Info("updated services") } } } +func (c *Config) reload() error { + ctx := context.Background() + + ctx, span := c.tracer.Start(ctx, "Config Reload") + defer span.End() + + if err := c.Load(); err != nil { + log.WithError(err).Error("error reloading config") + } + + log.WithField("services", c.Services).Info("config file updated") + + if err := c.executableSchema.UpdateServiceList(ctx, c.Services); err != nil { + log.WithError(err).Error("error updating services") + } + + log.WithField("services", c.Services).Info("updated services") + + return nil +} + // GetConfig returns operational config for the gateway func GetConfig(configFiles []string) (*Config, error) { watcher, err := fsnotify.NewWatcher() @@ -296,6 +315,7 @@ func GetConfig(configFiles []string) (*Config, error) { MaxServiceResponseSize: 1024 * 1024, watcher: watcher, + tracer: otel.GetTracerProvider().Tracer(instrumentationName), configFiles: configFiles, linkedFiles: linkedFiles, } @@ -343,13 +363,17 @@ func (c *Config) Init() error { services = append(services, NewService(s, serviceClientOptions...)) } - queryClientOptions := []ClientOpt{WithMaxResponseSize(c.MaxServiceResponseSize), WithUserAgent(GenerateUserAgent("query"))} + queryClientOptions := []ClientOpt{ + WithMaxResponseSize(c.MaxServiceResponseSize), + WithUserAgent(GenerateUserAgent("query")), + } + if c.QueryHTTPClient != nil { queryClientOptions = append(queryClientOptions, WithHTTPClient(c.QueryHTTPClient)) } queryClient := NewClientWithPlugins(c.plugins, queryClientOptions...) es := NewExecutableSchema(c.plugins, c.MaxRequestsPerQuery, queryClient, services...) - err = es.UpdateSchema(true) + err = es.UpdateSchema(context.Background(), true) if err != nil { return err } diff --git a/docs/configuration.md b/docs/configuration.md index 11a11501..a66c8f94 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,6 +17,12 @@ Sample configuration: "max-requests-per-query": 50, "max-client-response-size": 1048576, "id-field-name": "id", + "telemetry": { + "enabled": true, + "insecure": false, + "endpoint": "http://localhost:4317", + "serviceName": "bramble" + }, "plugins": [ { "name": "admin-ui" @@ -84,6 +90,21 @@ Sample configuration: - Default: `id` - Supports hot-reload: No +- `telemetry`: OpenTelemetry configuration. + - `enabled`: Enable OpenTelemetry. + - Default: `false` + - Supports hot-reload: No + - `insecure`: Whether to use insecure connection to OpenTelemetry collector. + - Default: `false` + - Supports hot-reload: No + - `endpoint`: OpenTelemetry collector endpoint. + - Default: If no endpoint is specified, telemetry is disabled. + - Supports hot-reload: No + - `serviceName`: Service name to use for OpenTelemetry. + - Default: `bramble` + - Supports hot-reload: No + + - `plugins`: Optional list of plugins to enable. See [plugins](plugins.md) for plugins-specific config. - Supports hot-reload: Partial. `Configure` method of previously enabled plugins will get called with new configuration. diff --git a/executable_schema.go b/executable_schema.go index 4533522a..cf76c761 100644 --- a/executable_schema.go +++ b/executable_schema.go @@ -11,6 +11,11 @@ import ( log "github.com/sirupsen/logrus" "github.com/vektah/gqlparser/v2/ast" "github.com/vektah/gqlparser/v2/gqlerror" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" ) @@ -30,6 +35,7 @@ func NewExecutableSchema(plugins []Plugin, maxRequestsPerQuery int64, client *Gr GraphqlClient: client, plugins: plugins, + tracer: otel.GetTracerProvider().Tracer(instrumentationName), MaxRequestsPerQuery: maxRequestsPerQuery, } } @@ -44,13 +50,23 @@ type ExecutableSchema struct { GraphqlClient *GraphQLClient MaxRequestsPerQuery int64 + tracer trace.Tracer mutex sync.RWMutex plugins []Plugin } // UpdateServiceList replaces the list of services with the provided one and // update the schema. -func (s *ExecutableSchema) UpdateServiceList(services []string) error { +func (s *ExecutableSchema) UpdateServiceList(ctx context.Context, services []string) error { + ctx, span := s.tracer.Start(ctx, "Federated Services Update", + trace.WithSpanKind(trace.SpanKindInternal), + trace.WithAttributes( + attribute.StringSlice("graphql.federation.services", services), + ), + ) + + defer span.End() + newServices := make(map[string]*Service) for _, svcURL := range services { if svc, ok := s.Services[svcURL]; ok { @@ -61,12 +77,12 @@ func (s *ExecutableSchema) UpdateServiceList(services []string) error { } s.Services = newServices - return s.UpdateSchema(true) + return s.UpdateSchema(ctx, true) } // UpdateSchema updates the schema from every service and then update the merged // schema. -func (s *ExecutableSchema) UpdateSchema(forceRebuild bool) error { +func (s *ExecutableSchema) UpdateSchema(ctx context.Context, forceRebuild bool) error { var services []*Service var schemas []*ast.Schema var updatedServices []string @@ -92,7 +108,7 @@ func (s *ExecutableSchema) UpdateSchema(forceRebuild bool) error { s := s_ group.Go(func() error { logger := log.WithField("url", url) - updated, err := s.Update() + updated, err := s.Update(ctx) if err != nil { promServiceUpdateErrorCounter.WithLabelValues(s.ServiceURL).Inc() promServiceUpdateErrorGauge.WithLabelValues(s.ServiceURL).Set(1) @@ -116,6 +132,7 @@ func (s *ExecutableSchema) UpdateSchema(forceRebuild bool) error { services = append(services, s) schemas = append(schemas, s.Schema) + return nil }) } @@ -155,6 +172,26 @@ func (s *ExecutableSchema) ExecuteQuery(ctx context.Context) *graphql.Response { operation := operationCtx.Operation variables := operationCtx.Variables + ctx, span := s.tracer.Start(ctx, "Federated GraphQL Query", + trace.WithSpanKind(trace.SpanKindInternal), + trace.WithAttributes( + semconv.GraphqlOperationTypeKey.String(string(operation.Operation)), + semconv.GraphqlOperationName(operationCtx.OperationName), + semconv.GraphqlDocument(operationCtx.RawQuery), + ), + ) + + defer span.End() + + traceErr := func(err error) { + if err == nil { + return + } + + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + for _, plugin := range s.plugins { plugin.InterceptRequest(ctx, operation.Name, operationCtx.RawQuery, variables) } @@ -185,6 +222,7 @@ func (s *ExecutableSchema) ExecuteQuery(ctx context.Context) *graphql.Response { Services: s.Services, }) if err != nil { + traceErr(err) return s.interceptResponse(ctx, operation.Name, operationCtx.RawQuery, variables, graphql.ErrorResponse(ctx, err.Error())) } @@ -212,8 +250,10 @@ func (s *ExecutableSchema) ExecuteQuery(ctx context.Context) *graphql.Response { executionStart := time.Now() qe := newQueryExecution(ctx, operationCtx.OperationName, s.GraphqlClient, filteredSchema, s.BoundaryQueries, int32(s.MaxRequestsPerQuery)) + results, executeErrs := qe.Execute(plan) if len(executeErrs) > 0 { + traceErr(executeErrs) return s.interceptResponse(ctx, operation.Name, operationCtx.RawQuery, variables, &graphql.Response{ Errors: executeErrs, }) @@ -241,7 +281,10 @@ func (s *ExecutableSchema) ExecuteQuery(ctx context.Context) *graphql.Response { mergedResult, err := mergeExecutionResults(results) if err != nil { errs = append(errs, &gqlerror.Error{Message: err.Error()}) + + traceErr(errs) AddField(ctx, "errors", errs) + return s.interceptResponse(ctx, operation.Name, operationCtx.RawQuery, variables, &graphql.Response{ Errors: errs, }) @@ -252,7 +295,10 @@ func (s *ExecutableSchema) ExecuteQuery(ctx context.Context) *graphql.Response { mergedResult = nil } else if err != nil { errs = append(errs, &gqlerror.Error{Message: err.Error()}) + + traceErr(errs) AddField(ctx, "errors", errs) + return s.interceptResponse(ctx, operation.Name, operationCtx.RawQuery, variables, &graphql.Response{ Errors: errs, }) @@ -266,6 +312,7 @@ func (s *ExecutableSchema) ExecuteQuery(ctx context.Context) *graphql.Response { timings["format"] = time.Since(formattingStart).String() if len(errs) > 0 { + traceErr(errs) AddField(ctx, "errors", errs) } diff --git a/execution.go b/execution.go index e4f54b92..f0d4fc22 100644 --- a/execution.go +++ b/execution.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "strings" "sync" "sync/atomic" @@ -100,8 +101,14 @@ func (q *queryExecution) executeRootStep(step *QueryPlanStep) error { return errors.New("expected mutation or query root step") } + req := NewRequest(document). + WithVariables(variables). + WithHeaders(GetOutgoingRequestHeadersFromContext(q.ctx)). + WithOperationName(q.operationName). + WithOperationType(step.ParentType) + var data map[string]interface{} - err := q.executeDocument(document, variables, step.ServiceURL, &data) + err := q.graphqlClient.Request(q.ctx, step.ServiceURL, req, &data) if err != nil { q.writeExecutionResult(step, data, err) return nil @@ -126,14 +133,6 @@ func (q *queryExecution) executeRootStep(step *QueryPlanStep) error { return nil } -func (q *queryExecution) executeDocument(query string, variables map[string]interface{}, serviceURL string, response interface{}) error { - req := NewRequest(query). - WithVariables(variables). - WithHeaders(GetOutgoingRequestHeadersFromContext(q.ctx)). - WithOperationName(q.operationName) - return q.graphqlClient.Request(q.ctx, serviceURL, req, &response) -} - func (q *queryExecution) writeExecutionResult(step *QueryPlanStep, data interface{}, err error) { result := executionResult{ ServiceURL: step.ServiceURL, @@ -212,8 +211,14 @@ func (q *queryExecution) executeBoundaryQuery(documents []string, serviceURL str output := make([]interface{}, 0) if !boundaryFieldGetter.Array { for _, document := range documents { + req := NewRequest(document). + WithVariables(variables). + WithHeaders(GetOutgoingRequestHeadersFromContext(q.ctx)). + WithOperationName(q.operationName). + WithOperationType(queryObjectName) + partialData := make(map[string]interface{}) - err := q.executeDocument(document, variables, serviceURL, &partialData) + err := q.graphqlClient.Request(q.ctx, serviceURL, req, &partialData) if err != nil { return nil, err } @@ -232,7 +237,13 @@ func (q *queryExecution) executeBoundaryQuery(documents []string, serviceURL str Result []interface{} `json:"_result"` }{} - err := q.executeDocument(documents[0], variables, serviceURL, &data) + req := NewRequest(documents[0]). + WithVariables(variables). + WithHeaders(GetOutgoingRequestHeadersFromContext(q.ctx)). + WithOperationName(q.operationName). + WithOperationType(queryObjectName) + + err := q.graphqlClient.Request(q.ctx, serviceURL, req, &data) return data.Result, err } @@ -258,7 +269,9 @@ func (q *queryExecution) createGQLErrors(step *QueryPlanStep, err error) gqlerro var gqlErr GraphqlErrors var outputErrs gqlerror.List - if errors.As(err, &gqlErr) { + + switch { + case errors.As(err, &gqlErr): for _, ge := range gqlErr { extensions := ge.Extensions if extensions == nil { @@ -270,21 +283,38 @@ func (q *queryExecution) createGQLErrors(step *QueryPlanStep, err error) gqlerro extensions["serviceUrl"] = step.ServiceURL outputErrs = append(outputErrs, &gqlerror.Error{ + Err: err, Message: ge.Message, Path: ge.Path, Locations: locs, Extensions: extensions, + Rule: "", }) } return outputErrs - } else { + + case os.IsTimeout(err): + outputErrs = append(outputErrs, &gqlerror.Error{ + Err: err, + Message: "downstream request timed out", + Path: path, + Locations: locs, + Extensions: map[string]interface{}{ + "selectionSet": formatSelectionSetSingleLine(q.ctx, q.schema, step.SelectionSet), + }, + Rule: "", + }) + + default: outputErrs = append(outputErrs, &gqlerror.Error{ + Err: err, Message: err.Error(), Path: path, Locations: locs, Extensions: map[string]interface{}{ "selectionSet": formatSelectionSetSingleLine(q.ctx, q.schema, step.SelectionSet), }, + Rule: "", }) } diff --git a/execution_introspection_test.go b/execution_introspection_test.go index 0defa15b..9a290b1e 100644 --- a/execution_introspection_test.go +++ b/execution_introspection_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" + "go.opentelemetry.io/otel/trace/noop" ) func TestIntrospectionQuery(t *testing.T) { @@ -55,6 +56,7 @@ func TestIntrospectionQuery(t *testing.T) { require.NoError(t, err) es := ExecutableSchema{ + tracer: noop.NewTracerProvider().Tracer("test"), MergedSchema: mergedSchema, } diff --git a/execution_test.go b/execution_test.go index 6a24f037..e4fb3438 100644 --- a/execution_test.go +++ b/execution_test.go @@ -8,7 +8,7 @@ import ( "io" "net/http" "net/http/httptest" - "regexp" + "os" "strings" "testing" "time" @@ -20,6 +20,7 @@ import ( "github.com/vektah/gqlparser/v2/ast" "github.com/vektah/gqlparser/v2/formatter" "github.com/vektah/gqlparser/v2/gqlerror" + "go.opentelemetry.io/otel/trace/noop" ) func TestHonorsPermissions(t *testing.T) { @@ -37,6 +38,7 @@ func TestHonorsPermissions(t *testing.T) { require.NoError(t, err) es := ExecutableSchema{ + tracer: noop.NewTracerProvider().Tracer("test"), MergedSchema: mergedSchema, } @@ -110,7 +112,8 @@ func TestQueryWithNamespace(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryError(t *testing.T) { @@ -169,7 +172,16 @@ func TestQueryError(t *testing.T) { } es := f.setup(t) - f.run(t, es) + f.run(t, es, func(t *testing.T, resp *graphql.Response) { + require.Equal(t, len(f.errors), len(resp.Errors)) + for i := range f.errors { + assert.Error(t, resp.Errors[i]) + assert.Equal(t, f.errors[i].Message, resp.Errors[i].Message, "error message did not match") + assert.Equal(t, f.errors[i].Path, resp.Errors[i].Path, "error path did not match") + assert.Equal(t, f.errors[i].Locations, resp.Errors[i].Locations, "error locations did not match") + assert.Equal(t, f.errors[i].Extensions, resp.Errors[i].Extensions, "error extensions did not match") + } + }) } func TestFederatedQueryFragmentSpreads(t *testing.T) { @@ -343,7 +355,8 @@ func TestFederatedQueryFragmentSpreads(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) t.Run("with overlap in field and fragment selection", func(t *testing.T) { @@ -374,7 +387,8 @@ func TestFederatedQueryFragmentSpreads(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) t.Run("with non abstract fragment", func(t *testing.T) { @@ -396,7 +410,8 @@ func TestFederatedQueryFragmentSpreads(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) t.Run("with named fragment spread", func(t *testing.T) { @@ -427,7 +442,8 @@ func TestFederatedQueryFragmentSpreads(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) t.Run("with nested fragment spread", func(t *testing.T) { @@ -460,7 +476,8 @@ func TestFederatedQueryFragmentSpreads(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) t.Run("with multiple implementation fragment spreads (gizmo implementation)", func(t *testing.T) { @@ -499,7 +516,8 @@ func TestFederatedQueryFragmentSpreads(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) t.Run("with multiple implementation fragment spreads (gadget implementation)", func(t *testing.T) { @@ -554,7 +572,8 @@ func TestFederatedQueryFragmentSpreads(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) t.Run("with multiple top level fragment spreads (gadget implementation)", func(t *testing.T) { @@ -607,7 +626,8 @@ func TestFederatedQueryFragmentSpreads(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) t.Run("with nested abstract fragment spreads", func(t *testing.T) { @@ -651,7 +671,8 @@ func TestFederatedQueryFragmentSpreads(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) } @@ -724,7 +745,8 @@ func TestQueryExecutionMultipleServices(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionServiceTimeout(t *testing.T) { @@ -802,7 +824,7 @@ func TestQueryExecutionServiceTimeout(t *testing.T) { }`, errors: gqlerror.List{ &gqlerror.Error{ - Message: `error during request: Post \"http://127.0.0.1:\d{5}\": context deadline exceeded`, + Message: "downstream request timed out", Path: ast.Path{ast.PathName("movie")}, Locations: []gqlerror.Location{ {Line: 5, Column: 5}, @@ -817,8 +839,26 @@ func TestQueryExecutionServiceTimeout(t *testing.T) { es := f.setup(t) es.GraphqlClient.HTTPClient.Timeout = 10 * time.Millisecond - f.run(t, es) - jsonEqWithOrder(t, f.expected, string(f.resp.Data)) + f.run(t, es, func(t *testing.T, resp *graphql.Response) { + jsonEqWithOrder(t, f.expected, string(resp.Data)) + + assert.Equal(t, len(f.errors), len(resp.Errors)) + + for i := range f.errors { + // We want to unwrap the error to check the underlying error + // type of the error returned by the client. This way we are + // able to check if the error is a timeout error. + respErr := resp.Errors[i].Unwrap() + + assert.Error(t, respErr, "expected error to be non-nil") + assert.True(t, os.IsTimeout(respErr), "expected timeout error, got %T", respErr) + assert.Equal(t, f.errors[i].Message, resp.Errors[i].Message, "error message did not match") + assert.Equal(t, f.errors[i].Path, resp.Errors[i].Path, "error path did not match") + assert.Equal(t, f.errors[i].Locations, resp.Errors[i].Locations, "error locations did not match") + assert.Equal(t, f.errors[i].Extensions, resp.Errors[i].Extensions, "error extensions did not match") + } + + }) } func TestQueryExecutionNamespaceAndFragmentSpread(t *testing.T) { @@ -921,7 +961,9 @@ func TestQueryExecutionNamespaceAndFragmentSpread(t *testing.T) { } es := f.setup(t) - f.run(t, es) + f.run(t, es, func(t *testing.T, resp *graphql.Response) { + jsonEqWithOrder(t, f.expected, string(resp.Data)) + }) } func TestQueryExecutionWithNullResponse(t *testing.T) { @@ -972,7 +1014,8 @@ func TestQueryExecutionWithNullResponse(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionWithSingleService(t *testing.T) { @@ -1014,7 +1057,8 @@ func TestQueryExecutionWithSingleService(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryWithArrayBoundaryFieldsAndMultipleChildrenSteps(t *testing.T) { @@ -1115,7 +1159,8 @@ func TestQueryWithArrayBoundaryFieldsAndMultipleChildrenSteps(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryWithBoundaryFieldsAndNullsAboveInsertionPoint(t *testing.T) { @@ -1229,7 +1274,8 @@ func TestQueryWithBoundaryFieldsAndNullsAboveInsertionPoint(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestNestingNullableBoundaryTypes(t *testing.T) { @@ -1334,7 +1380,8 @@ func TestNestingNullableBoundaryTypes(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) t.Run("nested boundary types sometimes null", func(t *testing.T) { @@ -1470,7 +1517,8 @@ func TestNestingNullableBoundaryTypes(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) }) } @@ -1516,7 +1564,8 @@ func TestQueryExecutionWithTypename(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionWithTypenameAndNamespaces(t *testing.T) { @@ -1588,7 +1637,8 @@ func TestQueryExecutionWithTypenameAndNamespaces(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionWithMultipleBoundaryQueries(t *testing.T) { @@ -1673,7 +1723,8 @@ func TestQueryExecutionWithMultipleBoundaryQueries(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionMultipleServicesWithArray(t *testing.T) { @@ -1827,7 +1878,8 @@ func TestQueryExecutionMultipleServicesWithArray(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionMultipleServicesWithEmptyArray(t *testing.T) { @@ -1878,7 +1930,8 @@ func TestQueryExecutionMultipleServicesWithEmptyArray(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionMultipleServicesWithNestedArrays(t *testing.T) { @@ -2000,7 +2053,8 @@ func TestQueryExecutionMultipleServicesWithNestedArrays(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionEmptyBoundaryResponse(t *testing.T) { @@ -2067,7 +2121,8 @@ func TestQueryExecutionEmptyBoundaryResponse(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionWithNullResponseAndSubBoundaryType(t *testing.T) { @@ -2123,7 +2178,8 @@ func TestQueryExecutionWithNullResponseAndSubBoundaryType(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionWithInputObject(t *testing.T) { @@ -2228,7 +2284,8 @@ func TestQueryExecutionWithInputObject(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionMultipleObjects(t *testing.T) { @@ -2323,7 +2380,8 @@ func TestQueryExecutionMultipleObjects(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionMultipleServicesWithSkipTrueDirectives(t *testing.T) { @@ -2388,7 +2446,8 @@ func TestQueryExecutionMultipleServicesWithSkipTrueDirectives(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionMultipleServicesWithSkipFalseDirectives(t *testing.T) { @@ -2474,7 +2533,8 @@ func TestQueryExecutionMultipleServicesWithSkipFalseDirectives(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionMultipleServicesWithIncludeFalseDirectives(t *testing.T) { @@ -2539,7 +2599,8 @@ func TestQueryExecutionMultipleServicesWithIncludeFalseDirectives(t *testing.T) }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionMultipleServicesWithIncludeTrueDirectives(t *testing.T) { @@ -2625,7 +2686,8 @@ func TestQueryExecutionMultipleServicesWithIncludeTrueDirectives(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestMutationExecution(t *testing.T) { @@ -2700,7 +2762,8 @@ func TestMutationExecution(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionWithUnions(t *testing.T) { @@ -2813,7 +2876,8 @@ func TestQueryExecutionWithUnions(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryExecutionWithNamespaces(t *testing.T) { @@ -2937,7 +3001,8 @@ func TestQueryExecutionWithNamespaces(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestDebugExtensions(t *testing.T) { @@ -2973,9 +3038,13 @@ func TestDebugExtensions(t *testing.T) { }`, } - f.checkSuccess(t) - assert.True(t, called) - assert.NotNil(t, f.resp.Extensions["variables"]) + es := f.setup(t) + f.run(t, es, func(t *testing.T, resp *graphql.Response) { + assert.True(t, called) + assert.NotNil(t, resp.Extensions["variables"]) + require.Empty(t, resp.Errors) + jsonEqWithOrder(t, f.expected, string(resp.Data)) + }) } func TestQueryWithBoundaryFields(t *testing.T) { @@ -3049,7 +3118,8 @@ func TestQueryWithBoundaryFields(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQuerySelectionSetFragmentMismatchesWithResponse(t *testing.T) { @@ -3098,7 +3168,8 @@ func TestQuerySelectionSetFragmentMismatchesWithResponse(t *testing.T) { } }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryWithArrayBoundaryFields(t *testing.T) { @@ -3212,7 +3283,8 @@ func TestQueryWithArrayBoundaryFields(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestQueryWithAbstractType(t *testing.T) { @@ -3308,7 +3380,8 @@ func TestQueryWithAbstractType(t *testing.T) { }`, } - f.checkSuccess(t) + es := f.setup(t) + f.run(t, es, f.checkSuccess()) } func TestMergeWithNull(t *testing.T) { @@ -3405,7 +3478,7 @@ func TestSchemaUpdate_serviceError(t *testing.T) { t.Error("expected both Gadget and Gizmo in schema") } - executableSchema.UpdateSchema(false) + executableSchema.UpdateSchema(context.TODO(), false) for _, service := range executableSchema.Services { if service.Name == "serviceA" { @@ -3431,19 +3504,10 @@ type queryExecutionFixture struct { mergedSchema *ast.Schema query string expected string - resp *graphql.Response debug *DebugInfo errors gqlerror.List } -func (f *queryExecutionFixture) checkSuccess(t *testing.T) { - es := f.setup(t) - f.run(t, es) - - require.Empty(t, f.resp.Errors) - jsonEqWithOrder(t, f.expected, string(f.resp.Data)) -} - func (f *queryExecutionFixture) setup(t *testing.T) *ExecutableSchema { var services []*Service var schemas []*ast.Schema @@ -3462,7 +3526,7 @@ func (f *queryExecutionFixture) setup(t *testing.T) *ExecutableSchema { } merged, err := MergeSchemas(schemas...) - require.NoError(t, err) + require.NoError(t, err, "failed to merge schemas before testrun") f.mergedSchema = merged @@ -3475,7 +3539,9 @@ func (f *queryExecutionFixture) setup(t *testing.T) *ExecutableSchema { return es } -func (f *queryExecutionFixture) run(t *testing.T, es *ExecutableSchema) { +type assertFunc func(t *testing.T, resp *graphql.Response) + +func (f *queryExecutionFixture) run(t *testing.T, es *ExecutableSchema, assertFunc assertFunc) { query := gqlparser.MustLoadQuery(f.mergedSchema, f.query) vars := f.variables if vars == nil { @@ -3485,22 +3551,21 @@ func (f *queryExecutionFixture) run(t *testing.T, es *ExecutableSchema) { if f.debug != nil { ctx = context.WithValue(ctx, DebugKey, *f.debug) } - f.resp = es.ExecuteQuery(ctx) - f.resp.Extensions = graphql.GetExtensions(ctx) - - if len(f.errors) == 0 { - require.Empty(t, f.resp.Errors) - jsonEqWithOrder(t, f.expected, string(f.resp.Data)) - } else { - require.Equal(t, len(f.errors), len(f.resp.Errors)) - for i := range f.errors { - delete(f.resp.Errors[i].Extensions, "serviceUrl") - // Allow regular expressions in expected error messages - if r, err := regexp.Compile(f.errors[i].Message); err == nil && r.Match([]byte(f.resp.Errors.Error())) { - f.errors[i].Message = f.resp.Errors[i].Message - } - require.Equal(t, *f.errors[i], *f.resp.Errors[i]) - } + resp := es.ExecuteQuery(ctx) + resp.Extensions = graphql.GetExtensions(ctx) + + // Remove serviceUrl from extensions to make tests deterministic + for i := range resp.Errors { + delete(resp.Errors[i].Extensions, "serviceUrl") + } + + assertFunc(t, resp) +} + +func (f *queryExecutionFixture) checkSuccess() assertFunc { + return func(t *testing.T, resp *graphql.Response) { + require.Empty(t, resp.Errors, "expected no errors, got %v", resp.Errors) + jsonEqWithOrder(t, f.expected, string(resp.Data)) } } diff --git a/format.go b/format.go index 6727caca..0f07caa8 100644 --- a/format.go +++ b/format.go @@ -14,10 +14,12 @@ import ( ) func indentPrefix(sb *strings.Builder, level int, suffix ...string) (int, error) { + sb.WriteString("\n") + var err error total, count := 0, 0 for i := 0; i <= level; i++ { - count, err = sb.WriteString(" ") + count, err = sb.WriteString(" ") total += count if err != nil { return total, err @@ -192,7 +194,7 @@ func formatSelectionSet(ctx context.Context, schema *ast.Schema, selection ast.S sb.WriteString("{") formatSelection(&sb, schema, vars, 0, selection) - sb.WriteString(" }") + sb.WriteString("\n}") return sb.String() } diff --git a/format_test.go b/format_test.go index c9296732..88ae41db 100644 --- a/format_test.go +++ b/format_test.go @@ -396,7 +396,7 @@ func TestFormatDocument(t *testing.T) { string(operationDefinition.Operation), operationDefinition.SelectionSet, ) - assert.Equal(t, `query { search(id: "123") { id title } }`, res) + assert.Equal(t, "query {\n search(id: \"123\") {\n id\n title\n }\n}", res) assert.Equal(t, (map[string]interface{})(nil), vars) } @@ -423,7 +423,7 @@ func TestFormatDocumentWithOperationName(t *testing.T) { string(operationDefinition.Operation), operationDefinition.SelectionSet, ) - assert.Equal(t, `query search{ search(id: "123") { id title } }`, res) + assert.Equal(t, "query search{\n search(id: \"123\") {\n id\n title\n }\n}", res) assert.Equal(t, (map[string]interface{})(nil), vars) } @@ -450,7 +450,7 @@ func TestFormatDocumentWithVariable(t *testing.T) { string(operationDefinition.Operation), operationDefinition.SelectionSet, ) - assert.Equal(t, `query search($id: ID!){ search(id: $id) { id title } }`, res) + assert.Equal(t, "query search($id: ID!){\n search(id: $id) {\n id\n title\n }\n}", res) assert.Equal(t, map[string]interface{}{"id": "123"}, vars) } @@ -477,7 +477,7 @@ func TestFormatDocumentWithListVariable(t *testing.T) { string(operationDefinition.Operation), operationDefinition.SelectionSet, ) - assert.Equal(t, `query search($ids: [ID!]){ search(ids: $ids) { id title } }`, res) + assert.Equal(t, "query search($ids: [ID!]){\n search(ids: $ids) {\n id\n title\n }\n}", res) assert.Equal(t, map[string]interface{}{"ids": `["123", "456"]`}, vars) } @@ -504,7 +504,7 @@ func TestFormatDocumentWithVariableWithinList(t *testing.T) { string(operationDefinition.Operation), operationDefinition.SelectionSet, ) - assert.Equal(t, `query search($id: ID!){ search(ids: ["123",$id,"789"]) { id title } }`, res) + assert.Equal(t, "query search($id: ID!){\n search(ids: [\"123\",$id,\"789\"]) {\n id\n title\n }\n}", res) assert.Equal(t, map[string]interface{}{"id": "123"}, vars) } @@ -535,7 +535,7 @@ func TestFormatDocumentWithInputVariable(t *testing.T) { string(operationDefinition.Operation), operationDefinition.SelectionSet, ) - assert.Equal(t, `query search($filter: Filter){ search(filter: $filter) { id title } }`, res) + assert.Equal(t, "query search($filter: Filter){\n search(filter: $filter) {\n id\n title\n }\n}", res) assert.Equal(t, map[string]interface{}{"filter": `{id: "123"}`}, vars) } @@ -566,7 +566,7 @@ func TestFormatDocumentWithVariableWithinInput(t *testing.T) { string(operationDefinition.Operation), operationDefinition.SelectionSet, ) - assert.Equal(t, `query search($id: ID!){ search(filter: {id:$id}) { id title } }`, res) + assert.Equal(t, "query search($id: ID!){\n search(filter: {id:$id}) {\n id\n title\n }\n}", res) assert.Equal(t, map[string]interface{}{"id": "123"}, vars) } @@ -601,6 +601,6 @@ func TestFormatDocumentWithVariableWithinNestedInput(t *testing.T) { string(operationDefinition.Operation), operationDefinition.SelectionSet, ) - assert.Equal(t, `query search($id: ID!){ search(filter: {sub:{id:$id}}) { id title } }`, res) + assert.Equal(t, "query search($id: ID!){\n search(filter: {sub:{id:$id}}) {\n id\n title\n }\n}", res) assert.Equal(t, map[string]interface{}{"id": "123"}, vars) } diff --git a/gateway.go b/gateway.go index c0ab272a..97fc2590 100644 --- a/gateway.go +++ b/gateway.go @@ -1,6 +1,7 @@ package bramble import ( + "context" "net/http" "time" @@ -8,6 +9,7 @@ import ( "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/transport" log "github.com/sirupsen/logrus" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) // Gateway contains the public and private routers @@ -29,7 +31,7 @@ func NewGateway(executableSchema *ExecutableSchema, plugins []Plugin) *Gateway { func (g *Gateway) UpdateSchemas(interval time.Duration) { time.Sleep(interval) for range time.Tick(interval) { - err := g.ExecutableSchema.UpdateSchema(false) + err := g.ExecutableSchema.UpdateSchema(context.Background(), false) if err != nil { log.WithError(err).Error("error updating schemas") } @@ -51,12 +53,7 @@ func (g *Gateway) Router(cfg *Config) http.Handler { gatewayHandler.Use(extension.Introspection{}) } - mux.Handle("/query", - applyMiddleware( - gatewayHandler, - debugMiddleware, - ), - ) + mux.Handle("/query", applyMiddleware(otelhttp.NewHandler(gatewayHandler, "/query"), debugMiddleware)) for _, plugin := range g.plugins { plugin.SetupPublicMux(mux) diff --git a/gateway_test.go b/gateway_test.go index 97499c22..7892247f 100644 --- a/gateway_test.go +++ b/gateway_test.go @@ -1,6 +1,7 @@ package bramble import ( + "context" "encoding/json" "fmt" "io" @@ -50,7 +51,7 @@ func TestGatewayQuery(t *testing.T) { })) client := NewClient(WithUserAgent(GenerateUserAgent("query"))) executableSchema := NewExecutableSchema(nil, 50, client, NewService(server.URL)) - err := executableSchema.UpdateSchema(true) + err := executableSchema.UpdateSchema(context.TODO(), true) require.NoError(t, err) gtw := NewGateway(executableSchema, []Plugin{}) rec := httptest.NewRecorder() diff --git a/go.mod b/go.mod index fd2c3026..9be55dcc 100644 --- a/go.mod +++ b/go.mod @@ -4,33 +4,31 @@ go 1.20 require ( github.com/99designs/gqlgen v0.17.41 - github.com/felixge/httpsnoop v1.0.2 + github.com/felixge/httpsnoop v1.0.4 github.com/fsnotify/fsnotify v1.5.1 github.com/golang-jwt/jwt/v4 v4.0.0 - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/graph-gophers/graphql-go v1.5.0 - github.com/kr/pretty v0.2.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/prometheus/client_golang v1.11.1 github.com/prometheus/common v0.31.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/rs/cors v1.7.0 github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/vektah/gqlparser/v2 v2.5.10 - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/sync v0.5.0 - golang.org/x/sys v0.15.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + golang.org/x/sys v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/square/go-jose.v2 v2.5.1 ) require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gofrs/uuid v4.2.0+incompatible github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect @@ -40,7 +38,29 @@ require ( ) require ( - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect github.com/sosodev/duration v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 + go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 + go.opentelemetry.io/otel/metric v1.21.0 // indirect + 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 +) + +require ( + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/grpc v1.59.0 // indirect ) diff --git a/go.sum b/go.sum index f875b282..5e2dbfc9 100644 --- a/go.sum +++ b/go.sum @@ -51,16 +51,17 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/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/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -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= @@ -70,8 +71,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF 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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= -github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +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.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -85,6 +86,9 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= @@ -93,6 +97,7 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= 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/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -118,8 +123,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD 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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -131,8 +136,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -144,14 +149,16 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE= @@ -170,12 +177,10 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -215,6 +220,7 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= @@ -227,15 +233,12 @@ github.com/sosodev/duration v1.1.0 h1:kQcaiGbJaIsRqgQy7VGlZrVw1giWO+lDoX3MCPnpVO github.com/sosodev/duration v1.1.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/vektah/gqlparser/v2 v2.5.10 h1:6zSM4azXC9u4Nxy5YmdmGu4uKamfwsdKTwp5zsEealU= github.com/vektah/gqlparser/v2 v2.5.10/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -246,16 +249,37 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +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.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= +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/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.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= +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= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -316,6 +340,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +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= @@ -370,8 +395,8 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= @@ -379,6 +404,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -425,7 +452,6 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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= 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= @@ -478,6 +504,11 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc 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-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 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= @@ -490,6 +521,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 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.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= 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= @@ -502,13 +535,13 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/introspection.go b/introspection.go index 90ff4e42..73c7af0a 100644 --- a/introspection.go +++ b/introspection.go @@ -6,6 +6,10 @@ import ( "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.opentelemetry.io/otel/trace" ) // Service is a federated service. @@ -17,6 +21,7 @@ type Service struct { Schema *ast.Schema Status string + tracer trace.Tracer client *GraphQLClient } @@ -25,15 +30,29 @@ func NewService(serviceURL string, opts ...ClientOpt) *Service { opts = append(opts, WithUserAgent(GenerateUserAgent("update"))) s := &Service{ ServiceURL: serviceURL, + tracer: otel.GetTracerProvider().Tracer(instrumentationName), client: NewClientWithoutKeepAlive(opts...), } return s } // Update queries the service's schema, name and version and updates its status. -func (s *Service) Update() (bool, error) { +func (s *Service) Update(ctx context.Context) (bool, error) { req := NewRequest("query brambleServicePoll { service { name, version, schema} }"). WithOperationName("brambleServicePoll") + + ctx, span := s.tracer.Start(ctx, "Federated Service Schema Update", + trace.WithSpanKind(trace.SpanKindInternal), + trace.WithAttributes( + semconv.GraphqlOperationTypeQuery, + semconv.GraphqlOperationName(req.OperationName), + semconv.GraphqlDocument(req.Query), + attribute.String("graphql.federation.service", s.Name), + ), + ) + + defer span.End() + response := struct { Service struct { Name string `json:"name"` @@ -42,7 +61,7 @@ func (s *Service) Update() (bool, error) { } `json:"service"` }{} - if err := s.client.Request(context.Background(), s.ServiceURL, req, &response); err != nil { + if err := s.client.Request(ctx, s.ServiceURL, req, &response); err != nil { s.SchemaSource = "" s.Status = "Unreachable" return false, err diff --git a/main.go b/main.go index bbdfd8fe..df4a52c1 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,8 @@ import ( // Main runs the gateway. This function is exported so that it can be reused // when building Bramble with custom plugins. func Main() { + ctx := context.Background() + var configFiles arrayFlags flag.Var(&configFiles, "config", "Config file (can appear multiple times)") flag.Var(&configFiles, "conf", "deprecated, use -config instead") @@ -28,6 +30,18 @@ func Main() { } go cfg.Watch() + shutdown, err := InitTelemetry(ctx, cfg.Telemetry) + if err != nil { + log.WithError(err).Error("error creating telemetry") + } + + defer func() { + log.Info("flushing and shutting down telemetry") + if err := shutdown(context.Background()); err != nil { + log.WithError(err).Error("shutting down telemetry") + } + }() + err = cfg.Init() if err != nil { log.WithError(err).Fatal("failed to configure") @@ -40,7 +54,7 @@ func Main() { go gtw.UpdateSchemas(cfg.PollIntervalDuration) - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) defer cancel() var wg sync.WaitGroup diff --git a/server_test.go b/server_test.go index d859989b..e30c85f1 100644 --- a/server_test.go +++ b/server_test.go @@ -1,6 +1,7 @@ package bramble import ( + "context" "testing" "github.com/movio/bramble/testsrv" @@ -14,7 +15,7 @@ func TestFederatedQuery(t *testing.T) { executableSchema := NewExecutableSchema(nil, 10, nil, NewService(gizmoService.URL), NewService(gadgetService.URL)) - require.NoError(t, executableSchema.UpdateSchema(true)) + require.NoError(t, executableSchema.UpdateSchema(context.TODO(), true)) query := gqlparser.MustLoadQuery(executableSchema.MergedSchema, `{ gizmo(id: "GIZMO1") { @@ -45,7 +46,7 @@ func TestFederatedQueryWithMultipleFragmentSpreads(t *testing.T) { executableSchema := NewExecutableSchema(nil, 10, nil, NewService(gizmoService.URL), NewService(gadgetService.URL)) - require.NoError(t, executableSchema.UpdateSchema(true)) + require.NoError(t, executableSchema.UpdateSchema(context.TODO(), true)) t.Run("first fragment matches", func(t *testing.T) { query := gqlparser.MustLoadQuery(executableSchema.MergedSchema, `{ diff --git a/telemetry.go b/telemetry.go new file mode 100644 index 00000000..f14ea6c7 --- /dev/null +++ b/telemetry.go @@ -0,0 +1,223 @@ +package bramble + +import ( + "context" + "errors" + + "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "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" +) + +// instrumentationName is used to identify the instrumentation in the +// OpenTelemetry collector. It maps to the attribute `otel.library.name`. +const instrumentationName string = "github.com/movio/bramble" + +// TelemetryConfig is the configuration for OpenTelemetry tracing and metrics. +type TelemetryConfig struct { + Enabled bool `json:"enabled"` // Enabled enables OpenTelemetry tracing and metrics. + Insecure bool `json:"insecure"` // Insecure enables insecure communication with the OpenTelemetry collector. + Endpoint string `json:"endpoint"` // Endpoint is the OpenTelemetry collector endpoint. + ServiceName string `json:"service_name"` // ServiceName is the name of the service. +} + +// TelemetryErrHandler is an error handler that logs errors. +type TelemetryErrHandler struct { + log *logrus.Logger +} + +// Handle implements otel.ErrorHandler. +func (e *TelemetryErrHandler) Handle(err error) { + e.log.Error(err.Error()) +} + +// InitializesTelemetry initializes OpenTelemetry tracing and metrics. It +// returns a shutdown function that should be called when the application +// terminates. +func InitTelemetry(ctx context.Context, cfg TelemetryConfig) (func(context.Context) error, error) { + // If telemetry is disabled, return a no-op shutdown function. The standard + // behaviour of the application will not be affected, since a + // `NoopTracerProvider` is used by default. + if !cfg.Enabled || cfg.Endpoint == "" { + return func(context.Context) error { return nil }, nil + } + + var flushAndShutdownFuncs []func(context.Context) error + + // flushAndShutdown calls cleanup functions registered via shutdownFuncs. + // The errors from the calls are joined. + // Each registered cleanup will be invoked once. + flushAndShutdown := func(ctx context.Context) error { + var err error + for _, fn := range flushAndShutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + flushAndShutdownFuncs = nil + return err + } + + // handleErr calls shutdown for cleanup and makes sure that all errors are returned. + handleErr := func(inErr error) error { + return errors.Join(inErr, flushAndShutdown(ctx)) + } + + if cfg.ServiceName == "" { + cfg.ServiceName = "bramble" + } + + // Set up resource. + res, err := resource.Merge(resource.Default(), + resource.NewWithAttributes(semconv.SchemaURL, + semconv.ServiceName(cfg.ServiceName), + semconv.ServiceVersion(Version), + )) + if err != nil { + return nil, handleErr(err) + } + + // Set up propagator. + prop := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) + + otel.SetTextMapPropagator(prop) + + errHandler := &TelemetryErrHandler{ + log: logrus.StandardLogger(), + } + + otel.SetErrorHandler(errHandler) + + traceShutdown, err := setupOTelTraceProvider(ctx, cfg, res) + if err != nil { + return nil, handleErr(err) + } + + flushAndShutdownFuncs = append(flushAndShutdownFuncs, traceShutdown...) + + meterShutdown, err := setupOTelMeterProvider(ctx, cfg, res) + if err != nil { + return nil, handleErr(err) + } + + flushAndShutdownFuncs = append(flushAndShutdownFuncs, meterShutdown...) + + return flushAndShutdown, nil +} + +func setupOTelTraceProvider(ctx context.Context, cfg TelemetryConfig, res *resource.Resource) ([]func(context.Context) error, error) { + // Set up exporter. + traceExp, err := newTraceExporter(ctx, cfg.Endpoint, cfg.Insecure) + if err != nil { + return nil, err + } + + // Set up trace provider. + tracerProvider, err := newTraceProvider(traceExp, res) + if err != nil { + return nil, err + } + + var shutdownFuncs []func(context.Context) error + shutdownFuncs = append(shutdownFuncs, + tracerProvider.ForceFlush, // ForceFlush exports any traces that have not yet been exported. + tracerProvider.Shutdown, // Shutdown stops the export pipeline and returns the last error. + ) + + otel.SetTracerProvider(tracerProvider) + return shutdownFuncs, nil +} + +func newTraceExporter(ctx context.Context, endpoint string, insecure bool) (sdktrace.SpanExporter, error) { + exporterOpts := []otlptracegrpc.Option{ + otlptracegrpc.WithEndpoint(endpoint), + } + + if insecure { + exporterOpts = append(exporterOpts, otlptracegrpc.WithInsecure()) + } + + traceExporter, err := otlptracegrpc.New(ctx, exporterOpts...) + if err != nil { + return nil, err + } + + return traceExporter, nil +} + +func newTraceProvider(exp sdktrace.SpanExporter, res *resource.Resource) (*sdktrace.TracerProvider, error) { + // ParentBased sampler is used to sample traces based on the parent span. + // This is useful for sampling traces based on the sampling decision of the + // upstream service. We follow the default sampling strategy of the + // OpenTelemetry Sampler. + parentSamplers := []sdktrace.ParentBasedSamplerOption{ + sdktrace.WithLocalParentSampled(sdktrace.AlwaysSample()), + sdktrace.WithLocalParentNotSampled(sdktrace.NeverSample()), + sdktrace.WithRemoteParentSampled(sdktrace.AlwaysSample()), + sdktrace.WithRemoteParentNotSampled(sdktrace.NeverSample()), + } + + traceProvider := sdktrace.NewTracerProvider( + // By default we'll trace all requests if not parent trace is found. + // Otherwise we follow the rules from above. + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.AlwaysSample(), parentSamplers...)), + sdktrace.WithResource(res), + sdktrace.WithSpanProcessor(sdktrace.NewBatchSpanProcessor(exp)), + ) + + return traceProvider, nil +} + +func setupOTelMeterProvider(ctx context.Context, cfg TelemetryConfig, res *resource.Resource) ([]func(context.Context) error, error) { + metricExp, err := newMetricExporter(ctx, cfg.Endpoint, cfg.Insecure) + if err != nil { + return nil, err + } + + meterProvider, err := newMeterProvider(metricExp, res) + if err != nil { + return nil, err + } + + var shutdownFuncs []func(context.Context) error + shutdownFuncs = append(shutdownFuncs, + meterProvider.ForceFlush, // ForceFlush exports any metrics that have not yet been exported. + meterProvider.Shutdown, // Shutdown stops the export pipeline and returns the last error. + ) + + otel.SetMeterProvider(meterProvider) + return shutdownFuncs, nil +} + +func newMetricExporter(ctx context.Context, endpoint string, insecure bool) (sdkmetric.Exporter, error) { + exporterOpts := []otlpmetricgrpc.Option{ + otlpmetricgrpc.WithEndpoint(endpoint), + } + + if insecure { + exporterOpts = append(exporterOpts, otlpmetricgrpc.WithInsecure()) + } + + metricExporter, err := otlpmetricgrpc.New(ctx, exporterOpts...) + if err != nil { + return nil, err + } + + return metricExporter, nil +} + +func newMeterProvider(exp sdkmetric.Exporter, res *resource.Resource) (*sdkmetric.MeterProvider, error) { + meterProvider := sdkmetric.NewMeterProvider( + sdkmetric.WithResource(res), + sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exp)), + ) + + return meterProvider, nil +}