Skip to content

Commit

Permalink
Merge branch 'task-open-telemetry' into 'main'
Browse files Browse the repository at this point in the history
Add open telemetry

See merge request networkteam/go-apibackend-boilerplate!6
  • Loading branch information
hlubek committed Sep 17, 2024
2 parents e8a3365 + 385eaca commit 6e2695d
Show file tree
Hide file tree
Showing 23 changed files with 523 additions and 104 deletions.
2 changes: 2 additions & 0 deletions backend/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Enable OpenTelemetry for development by setting this to 1
OPEN_TELEMETRY_ENABLED=0
3 changes: 2 additions & 1 deletion backend/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ run:
tests: true

output:
format: line-number
formats:
- format: line-number

linters:
# Disable all linters, so we enable linters explicitly
Expand Down
42 changes: 21 additions & 21 deletions backend/api/graph/admin.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions backend/api/graph/authentication.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions backend/api/graph/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,27 @@ package graph

import (
"myvendor.mytld/myproject/backend/api"
"myvendor.mytld/myproject/backend/finder"
"myvendor.mytld/myproject/backend/handler"
)

type Resolver struct {
api.ResolverDependencies
api.ResolverConfig

handler *handler.Handler
finder *finder.Finder
}

func NewResolver(deps api.ResolverDependencies, config api.ResolverConfig) *Resolver {
return &Resolver{
ResolverDependencies: deps,
ResolverConfig: config,
handler: handler.NewHandler(deps.DB, deps.Config, handler.Deps{
TimeSource: deps.TimeSource,
Mailer: deps.Mailer,
MeterProvider: deps.MeterProvider,
}),
finder: finder.NewFinder(deps.DB, deps.TimeSource),
}
}
13 changes: 11 additions & 2 deletions backend/api/graph/tests/authentication/mutation_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"myvendor.mytld/myproject/backend/test"
test_db "myvendor.mytld/myproject/backend/test/db"
test_graphql "myvendor.mytld/myproject/backend/test/graphql"
test_telemetry "myvendor.mytld/myproject/backend/test/telemetry"
)

const loginGQL = `
Expand Down Expand Up @@ -71,8 +72,10 @@ func TestMutationResolver_Login_WithSystemAdministrator_Valid(t *testing.T) {

var result loginResult

metricsReader, meterProvider := test_telemetry.SetupTestMeter(t)

req := test_graphql.NewRequest(t, query)
resp := test_graphql.Handle(t, api.ResolverDependencies{DB: db, TimeSource: timeSource}, req, &result)
resp := test_graphql.Handle(t, api.ResolverDependencies{DB: db, TimeSource: timeSource, MeterProvider: meterProvider}, req, &result)
test_graphql.RequireNoErrors(t, result.GraphqlErrors)

require.Nil(t, result.Data.Result.Error)
Expand All @@ -85,6 +88,8 @@ func TestMutationResolver_Login_WithSystemAdministrator_Valid(t *testing.T) {
setCookieHeader := resp.Header().Get("Set-Cookie")
assert.NotEmpty(t, setCookieHeader, "Set-Cookie header is set")

test_telemetry.AssertMeterCounter(t, metricsReader, "myvendor.mytld/myproject/backend/handler", "login.success.counter", 1)

// Test we can use a restricted field after authentication

var loginStatusResult struct {
Expand Down Expand Up @@ -123,12 +128,16 @@ func TestMutationResolver_Login_WithSystemAdministrator_InvalidPassword(t *testi

var result loginResult

metricsReader, meterProvider := test_telemetry.SetupTestMeter(t)

req := test_graphql.NewRequest(t, query)
test_graphql.Handle(t, api.ResolverDependencies{DB: db, TimeSource: timeSource}, req, &result)
test_graphql.Handle(t, api.ResolverDependencies{DB: db, TimeSource: timeSource, MeterProvider: meterProvider}, req, &result)
test_graphql.RequireNoErrors(t, result.GraphqlErrors)

require.NotNil(t, result.Data.Result.Error, "result.error")
assert.Equal(t, "invalidCredentials", result.Data.Result.Error.Code, "result.error.code")

test_telemetry.AssertMeterCounter(t, metricsReader, "myvendor.mytld/myproject/backend/handler", "login.failed.counter", 1)
}

func TestMutationResolver_Login_WithOrganisationAdministrator_Valid(t *testing.T) {
Expand Down
39 changes: 32 additions & 7 deletions backend/api/handler/graphql_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"context"
"fmt"
"net/http"
"time"

Expand All @@ -12,6 +13,8 @@ import (
"github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/gorilla/websocket"
"github.com/ravilushqa/otelgqlgen"
"go.opentelemetry.io/otel/attribute"

"myvendor.mytld/myproject/backend/api"
"myvendor.mytld/myproject/backend/api/graph"
Expand All @@ -23,23 +26,25 @@ import (
type Config struct {
EnableTracing bool
EnableLogging bool
EnableOpenTelemetry bool
DisableRecover bool
WebsocketAllowOrigin string
// Constant time duration for sensitive operations (e.g. login / request password reset / perform password reset / registration)
SensitiveOperationConstantTime time.Duration
}

const (
requestVariablesPrefix = "gql.request.variables"
)

func NewGraphqlHandler(deps api.ResolverDependencies, handlerConfig Config) http.Handler {
config := generated.Config{
Resolvers: &graph.Resolver{
ResolverDependencies: deps,
ResolverConfig: api.ResolverConfig{
SensitiveOperationConstantTime: handlerConfig.SensitiveOperationConstantTime,
},
},
Resolvers: graph.NewResolver(deps, api.ResolverConfig{
SensitiveOperationConstantTime: handlerConfig.SensitiveOperationConstantTime,
}),
Directives: generated.DirectiveRoot{
// No op implementation, will be checked in middleware
BypassAuthentication: func(ctx context.Context, obj any, next graphql.Resolver) (res any, err error) {
BypassAuthentication: func(ctx context.Context, _ any, next graphql.Resolver) (res any, err error) {
return next(ctx)
},
},
Expand All @@ -48,6 +53,26 @@ func NewGraphqlHandler(deps api.ResolverDependencies, handlerConfig Config) http
srv := newDefaultServer(exec, handlerConfig)
srv.SetErrorPresenter(ErrorPresenter)

if handlerConfig.EnableOpenTelemetry {
srv.Use(otelgqlgen.Middleware(
otelgqlgen.WithRequestVariablesAttributesBuilder(
func(requestVariables map[string]any) []attribute.KeyValue {
variables := make([]attribute.KeyValue, 0, len(requestVariables))
for k, v := range requestVariables {
switch k {
case "password":
v = "********"
}
variables = append(variables,
attribute.String(fmt.Sprintf("%s.%s", requestVariablesPrefix, k), fmt.Sprintf("%+v", v)),
)
}
return variables
},
),
))
}

if handlerConfig.EnableLogging {
srv.AroundFields(graphql_middleware.LoggerFieldMiddleware)
}
Expand Down
24 changes: 7 additions & 17 deletions backend/api/resolver_dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,17 @@ package api
import (
"database/sql"

"go.opentelemetry.io/otel/metric"

"myvendor.mytld/myproject/backend/domain"
"myvendor.mytld/myproject/backend/finder"
"myvendor.mytld/myproject/backend/handler"
"myvendor.mytld/myproject/backend/mail"
)

// ResolverDependencies provides common dependencies for api resolvers
type ResolverDependencies struct {
DB *sql.DB
TimeSource domain.TimeSource
Mailer *mail.Mailer
Config domain.Config
}

func (r ResolverDependencies) Handler() *handler.Handler {
return handler.NewHandler(r.DB, r.Config, handler.Deps{
TimeSource: r.TimeSource,
Mailer: r.Mailer,
})
}

func (r ResolverDependencies) Finder() *finder.Finder {
return finder.NewFinder(r.DB, r.TimeSource)
Config domain.Config
DB *sql.DB
TimeSource domain.TimeSource
MeterProvider metric.MeterProvider
Mailer *mail.Mailer
}
Loading

0 comments on commit 6e2695d

Please sign in to comment.