Skip to content

Commit

Permalink
Merge pull request #34 from Interhyp/33-add-elastic-apm-support
Browse files Browse the repository at this point in the history
feat(#33): add elastic APM
  • Loading branch information
KRaffael authored Nov 2, 2022
2 parents 03d54ef + d9ffc75 commit 9bfa33a
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 16 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ After this, the application can be started with:

`go run main.go`

### use Elastic APM during local development

To use Elastic APM add the following environment to your (run) configuration:

`ELASTIC_APM_SERVER_URL=https://apm-server.sys.ehyp.dev.interhyp-cloud.de;ELASTIC_APM_ENVIRONMENT=dev;ELASTIC_APM_SERVICE_NAME=metadata`

To disable APM, even if it is configured, add `ELASTIC_APM_DISABLED: true` to your `local-config.yaml`.

### swagger-ui

This service comes with the swagger ui built-in.
Expand Down
2 changes: 2 additions & 0 deletions acorns/repository/customconfigint.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ type CustomConfiguration interface {
AlertTargetSuffix() string

AdditionalPromotersFromOwners() []string

ElasticApmEnabled() bool
}

// Custom is a type casting helper that gets you from the configuration acorn to your CustomConfiguration
Expand Down
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ require (
github.com/rs/zerolog v1.28.0
github.com/stretchr/testify v1.8.1
github.com/twmb/franz-go v1.9.0
go.elastic.co/apm/module/apmchiv5/v2 v2.1.0
go.elastic.co/apm/module/apmhttp/v2 v2.1.0
go.elastic.co/apm/v2 v2.1.0
gopkg.in/yaml.v3 v3.0.1
)

Expand All @@ -80,10 +83,14 @@ require (
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
github.com/StephanHCB/go-autumn-web-swagger-ui v0.2.3 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/elastic/go-licenser v0.4.0 // indirect
github.com/elastic/go-sysinfo v1.7.1 // indirect
github.com/elastic/go-windows v1.0.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect
Expand All @@ -92,6 +99,8 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jcchavezs/porto v0.1.0 // indirect
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
Expand All @@ -104,20 +113,27 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
github.com/tidwall/tinylru v1.1.0 // indirect
github.com/twmb/franz-go/pkg/kmsg v1.2.0 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
go.elastic.co/fastjson v1.1.0 // indirect
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/tools v0.1.9 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
howett.net/plist v1.0.0 // indirect
)
52 changes: 51 additions & 1 deletion go.sum

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion internal/repository/config/accessors.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package config

import "strings"
import (
"os"
"strings"
)

func (c *CustomConfigImpl) BbUser() string {
return c.VBbUser
Expand Down Expand Up @@ -69,3 +72,10 @@ func (c *CustomConfigImpl) AlertTargetSuffix() string {
func (c *CustomConfigImpl) AdditionalPromotersFromOwners() []string {
return strings.Split(c.VAdditionalPromoters, ",")
}

func (c *CustomConfigImpl) ElasticApmEnabled() bool {
return !c.VElasticApmDisabled &&
os.Getenv("ELASTIC_APM_SERVER_URL") != "" &&
os.Getenv("ELASTIC_APM_SERVICE_NAME") != "" &&
os.Getenv("ELASTIC_APM_ENVIRONMENT") != ""
}
13 changes: 13 additions & 0 deletions internal/repository/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
auconfigapi "github.com/StephanHCB/go-autumn-config-api"
auconfigenv "github.com/StephanHCB/go-autumn-config-env"
"strconv"
)

const (
Expand All @@ -23,6 +24,7 @@ const (
KeyAlertTargetPrefix = "ALERT_TARGET_PREFIX"
KeyAlertTargetSuffix = "ALERT_TARGET_SUFFIX"
KeyAdditionalPromoters = "ADDITIONAL_PROMOTERS_FROM_OWNERS"
KeyElasticApmDisabled = "ELASTIC_APM_DISABLED"
)

var CustomConfigItems = []auconfigapi.ConfigItem{
Expand Down Expand Up @@ -143,4 +145,15 @@ var CustomConfigItems = []auconfigapi.ConfigItem{
Description: "owner aliases from which to get additional promoters to be added for all services. Can be left empty, or contain a comma separated list of owner aliases",
Validate: auconfigenv.ObtainPatternValidator("^|[a-z](-?[a-z0-9]+)*(,[a-z](-?[a-z0-9]+)*)*$"),
},
{
Key: KeyElasticApmDisabled,
EnvName: KeyElasticApmDisabled,
Default: "false",
Description: "Disable Elastic APM middleware. Supports all values supported by ParseBool (https://pkg.go.dev/strconv#ParseBool).",
Validate: func(key string) error {
value := auconfigenv.Get(key)
_, err := strconv.ParseBool(value)
return err
},
},
}
3 changes: 3 additions & 0 deletions internal/repository/config/plumbing.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
auacornapi "github.com/StephanHCB/go-autumn-acorn-registry/api"
auconfigenv "github.com/StephanHCB/go-autumn-config-env"
libconfig "github.com/StephanHCB/go-backend-service-common/repository/config"
"strconv"
)

type CustomConfigImpl struct {
Expand All @@ -24,6 +25,7 @@ type CustomConfigImpl struct {
VAlertTargetPrefix string
VAlertTargetSuffix string
VAdditionalPromoters string
VElasticApmDisabled bool
}

func New() auacornapi.Acorn {
Expand All @@ -49,6 +51,7 @@ func (c *CustomConfigImpl) Obtain(getter func(key string) string) {
c.VAlertTargetPrefix = getter(KeyAlertTargetPrefix)
c.VAlertTargetSuffix = getter(KeyAlertTargetSuffix)
c.VAdditionalPromoters = getter(KeyAdditionalPromoters)
c.VElasticApmDisabled, _ = strconv.ParseBool(getter(KeyElasticApmDisabled))
}

// used after validation, so known safe
Expand Down
4 changes: 4 additions & 0 deletions internal/service/services/services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ func tstCreateValid() openapi.ServiceCreateDto {
type MockConfig struct {
}

func (c *MockConfig) ElasticApmEnabled() bool {
return false
}

func (c *MockConfig) BbUser() string {
//TODO implement me
panic("implement me")
Expand Down
80 changes: 80 additions & 0 deletions web/middleware/apmtraceparent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package middleware

// Elastic APM license: https://pkg.go.dev/go.elastic.co/apm/module/apmhttp/v2?tab=licenses

import (
cryptorand "crypto/rand"
"encoding/binary"
"github.com/StephanHCB/go-backend-service-common/web/middleware/requestid"
"go.elastic.co/apm/module/apmhttp/v2"
"go.elastic.co/apm/v2"
"math/rand"
"net/http"
"time"
)

var TraceContextFetcherForResponseHeaders = RestoreOrCreateTraceContextWithoutAPM

// RestoreOrCreateTraceContextWithoutAPM is designed as a fallback to enable trace propagation even if Elastic APM is not configured or disabled.
// It uses some Elastic apm go agent functions for compatibility but does not require the middleware to be active.
func RestoreOrCreateTraceContextWithoutAPM(r *http.Request) apm.TraceContext {
// create own trace context
traceContext, ok := getRequestTraceparent(r)
if ok {
traceContext.State, _ = apmhttp.ParseTracestateHeader(r.Header[apmhttp.TracestateHeader]...)
} else {
// no trace context restored from header -> we create our own
// code mostly copied from https://pkg.go.dev/go.elastic.co/apm/[email protected]#Tracer.StartTransactionOptions
var seed int64
if err := binary.Read(cryptorand.Reader, binary.LittleEndian, &seed); err != nil {
seed = time.Now().UnixNano()
}
random := rand.New(rand.NewSource(seed))
binary.LittleEndian.PutUint64(traceContext.Trace[:8], random.Uint64())
binary.LittleEndian.PutUint64(traceContext.Trace[8:], random.Uint64())
copy(traceContext.Span[:], traceContext.Trace[:])
//we do not set trace state by ourselves as it is vendor specific and contains the sample rate for Elastic apm
traceContext.Options.WithRecorded(false) //do not record our custom IDs
}
return traceContext
}

// UseElasticApmTraceContext extracts the apm.TraceContext from the apm.Transaction stored in the Context. The transaction
// is contained in the context if Elastic APM is enabled and the apmhttp handler has started the transaction.
// See https://pkg.go.dev/go.elastic.co/apm/module/apmhttp/[email protected]#StartTransactionWithBody
func UseElasticApmTraceContext(r *http.Request) apm.TraceContext {
ctx := r.Context()
return apm.TransactionFromContext(ctx).TraceContext()
}

func ApmTraceResponseHeaders(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var traceContext = TraceContextFetcherForResponseHeaders(r)
ctx := r.Context()

//add traceparent as request id to context
//(response header is added later by middleware requestidinresponse.AddRequestIdHeaderToResponse)
requestId := apmhttp.FormatTraceparentHeader(traceContext)
requestIdCtx := requestid.PutReqID(ctx, requestId)

//propagate tracestate (containing sampling rate, vendor etc.) if present in the trace context
//I appreciate a more consistent solution (one id is added to context, other is added as header directly)
if tracestate := traceContext.State.String(); tracestate != "" {
w.Header().Set(apmhttp.TracestateHeader, tracestate)
}

next.ServeHTTP(w, r.WithContext(requestIdCtx))
}
return http.HandlerFunc(fn)
}

// copied (and slightly modified) from pkg\mod\go.elastic.co\apm\module\apmhttp\[email protected]\handler.go
// why the hell is this private? :/
func getRequestTraceparent(req *http.Request) (apm.TraceContext, bool) {
if values := req.Header[apmhttp.W3CTraceparentHeader]; len(values) == 1 && values[0] != "" {
if c, err := apmhttp.ParseTraceparentHeader(values[0]); err == nil {
return c, true
}
}
return apm.TraceContext{}, false
}
2 changes: 2 additions & 0 deletions web/server/acorn.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ func (s *Impl) SetupAcorn(registry auacornapi.AcornRegistry) error {
return err
}

s.CustomConfiguration = repository.Custom(s.Configuration)

ctx := auzerolog.AddLoggerToCtx(context.Background())

s.WireUp(ctx)
Expand Down
38 changes: 24 additions & 14 deletions web/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"github.com/StephanHCB/go-backend-service-common/web/middleware/timeout"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"go.elastic.co/apm/module/apmchiv5/v2"
"go.elastic.co/apm/module/apmhttp/v2"
"net"
"net/http"
"os"
Expand All @@ -29,16 +31,17 @@ import (
)

type Impl struct {
Logging librepo.Logging
Configuration librepo.Configuration
Vault repository.Vault
IdentityProvider repository.IdentityProvider
HealthCtl libcontroller.HealthController
SwaggerCtl libcontroller.SwaggerController
OwnerCtl controller.OwnerController
ServiceCtl controller.ServiceController
RepositoryCtl controller.RepositoryController
WebhookCtl controller.WebhookController
Logging librepo.Logging
Configuration librepo.Configuration
CustomConfiguration repository.CustomConfiguration
Vault repository.Vault
IdentityProvider repository.IdentityProvider
HealthCtl libcontroller.HealthController
SwaggerCtl libcontroller.SwaggerController
OwnerCtl controller.OwnerController
ServiceCtl controller.ServiceController
RepositoryCtl controller.RepositoryController
WebhookCtl controller.WebhookController

Router chi.Router
}
Expand All @@ -48,13 +51,20 @@ func (s *Impl) WireUp(ctx context.Context) {
s.Logging.Logger().Ctx(ctx).Info().Print("creating router and setting up filter chain")
s.Router = chi.NewRouter()

// generate request id (or read from request header if present) and add it to request context
requestid.RequestIDHeader = "X-B3-TraceId"
s.Router.Use(requestid.RequestID)

requestid.RequestIDHeader = apmhttp.W3CTraceparentHeader
loggermiddleware.RequestIdFieldName = "trace.id"
loggermiddleware.MethodFieldName = "http.request.method"
loggermiddleware.PathFieldName = "url.path"

if s.CustomConfiguration.ElasticApmEnabled() {
middleware.TraceContextFetcherForResponseHeaders = middleware.UseElasticApmTraceContext
//TODO disable panic propagation (?)
s.Router.Use(apmchiv5.Middleware())
} else {
middleware.TraceContextFetcherForResponseHeaders = middleware.RestoreOrCreateTraceContextWithoutAPM
s.Logging.Logger().NoCtx().Warn().Printf("Elastic APM not configured or disabled, skipping middleware.")
}

// build a request specific logger (includes request id and some fields) and add it to the request context
s.Router.Use(loggermiddleware.AddZerologLoggerToContext)

Expand Down

0 comments on commit 9bfa33a

Please sign in to comment.