Added the files for this package.
This commit is contained in:
parent
4424b06532
commit
d56374a8f7
32 changed files with 2179 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/.idea/
|
208
config.go
Normal file
208
config.go
Normal file
|
@ -0,0 +1,208 @@
|
|||
package otelchi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
otelmetric "go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// These defaults are used in `TraceHeaderConfig`.
|
||||
const (
|
||||
DefaultTraceIDResponseHeaderKey = "X-Trace-Id"
|
||||
DefaultTraceSampledResponseHeaderKey = "X-Trace-Sampled"
|
||||
)
|
||||
|
||||
// config is used to configure the mux middleware.
|
||||
type config struct {
|
||||
TracerProvider oteltrace.TracerProvider
|
||||
MeterProvider otelmetric.MeterProvider
|
||||
Propagators propagation.TextMapPropagator
|
||||
ChiRoutes chi.Routes
|
||||
RequestMethodInSpanName bool
|
||||
Filters []Filter
|
||||
TraceIDResponseHeaderKey string
|
||||
TraceSampledResponseHeaderKey string
|
||||
PublicEndpointFn func(r *http.Request) bool
|
||||
|
||||
DisableMeasureInflight bool
|
||||
DisableMeasureSize bool
|
||||
}
|
||||
|
||||
// Option specifies instrumentation configuration options.
|
||||
type Option interface {
|
||||
apply(*config)
|
||||
}
|
||||
|
||||
type optionFunc func(*config)
|
||||
|
||||
func (o optionFunc) apply(c *config) {
|
||||
o(c)
|
||||
}
|
||||
|
||||
// Filter is a predicate used to determine whether a given http.Request should
|
||||
// be traced. A Filter must return true if the request should be traced.
|
||||
type Filter func(*http.Request) bool
|
||||
|
||||
// WithPropagators specifies propagators to use for extracting
|
||||
// information from the HTTP requests. If none are specified, global
|
||||
// ones will be used.
|
||||
func WithPropagators(propagators propagation.TextMapPropagator) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.Propagators = propagators
|
||||
})
|
||||
}
|
||||
|
||||
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
|
||||
// If none is specified, the global provider is used.
|
||||
func WithTracerProvider(provider oteltrace.TracerProvider) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.TracerProvider = provider
|
||||
})
|
||||
}
|
||||
|
||||
// WithMeterProvider specifies a meter provider to use for creating a meter.
|
||||
// If none is specified, the global provider is used.
|
||||
func WithMeterProvider(provider otelmetric.MeterProvider) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.MeterProvider = provider
|
||||
})
|
||||
}
|
||||
|
||||
// WithMeasureInflightDisabled specifies whether to measure the number of inflight requests.
|
||||
// If this option is not set, the number of inflight requests will be measured.
|
||||
func WithMeasureInflightDisabled(isDisabled bool) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.DisableMeasureInflight = isDisabled
|
||||
})
|
||||
}
|
||||
|
||||
// WithMeasureSizeDisabled specifies whether to measure the size of the response body.
|
||||
// If this option is not set, the size of the response body will be measured.
|
||||
func WithMeasureSizeDisabled(isDisabled bool) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.DisableMeasureSize = isDisabled
|
||||
})
|
||||
}
|
||||
|
||||
// WithChiRoutes specified the routes that being used by application. Its main
|
||||
// purpose is to provide route pattern as span name during span creation. If this
|
||||
// option is not set, by default the span will be given name at the end of span
|
||||
// execution. For some people, this behavior is not desirable since they want
|
||||
// to override the span name on underlying handler. By setting this option, it
|
||||
// is possible for them to override the span name.
|
||||
func WithChiRoutes(routes chi.Routes) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.ChiRoutes = routes
|
||||
})
|
||||
}
|
||||
|
||||
// WithRequestMethodInSpanName is used for adding http request method to span name.
|
||||
// While this is not necessary for vendors that properly implemented the tracing
|
||||
// specs (e.g Jaeger, AWS X-Ray, etc...), but for other vendors such as Elastic
|
||||
// and New Relic this might be helpful.
|
||||
//
|
||||
// See following threads for details:
|
||||
//
|
||||
// - https://github.com/riandyrn/otelchi/pull/3#issuecomment-1005883910
|
||||
// - https://github.com/riandyrn/otelchi/issues/6#issuecomment-1034461912
|
||||
func WithRequestMethodInSpanName(isActive bool) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.RequestMethodInSpanName = isActive
|
||||
})
|
||||
}
|
||||
|
||||
// WithFilter adds a filter to the list of filters used by the handler.
|
||||
// If any filter indicates to exclude a request then the request will not be
|
||||
// traced. All filters must allow a request to be traced for a Span to be created.
|
||||
// If no filters are provided then all requests are traced.
|
||||
// Filters will be invoked for each processed request, it is advised to make them
|
||||
// simple and fast.
|
||||
func WithFilter(filter Filter) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.Filters = append(cfg.Filters, filter)
|
||||
})
|
||||
}
|
||||
|
||||
// WithTraceIDResponseHeader enables adding trace id into response header.
|
||||
// It accepts a function that generates the header key name. If this parameter
|
||||
// function set to `nil` the default header key which is `X-Trace-Id` will be used.
|
||||
//
|
||||
// Deprecated: use `WithTraceResponseHeaders` instead.
|
||||
func WithTraceIDResponseHeader(headerKeyFunc func() string) Option {
|
||||
cfg := TraceHeaderConfig{
|
||||
TraceIDHeader: "",
|
||||
TraceSampledHeader: "",
|
||||
}
|
||||
if headerKeyFunc != nil {
|
||||
cfg.TraceIDHeader = headerKeyFunc()
|
||||
}
|
||||
return WithTraceResponseHeaders(cfg)
|
||||
}
|
||||
|
||||
// TraceHeaderConfig is configuration for trace headers in the response.
|
||||
type TraceHeaderConfig struct {
|
||||
TraceIDHeader string // if non-empty overrides the default of X-Trace-ID
|
||||
TraceSampledHeader string // if non-empty overrides the default of X-Trace-Sampled
|
||||
}
|
||||
|
||||
// WithTraceResponseHeaders configures the response headers for trace information.
|
||||
// It accepts a TraceHeaderConfig struct that contains the keys for the Trace ID
|
||||
// and Trace Sampled headers. If the provided keys are empty, default values will
|
||||
// be used for the respective headers.
|
||||
func WithTraceResponseHeaders(cfg TraceHeaderConfig) Option {
|
||||
return optionFunc(func(c *config) {
|
||||
c.TraceIDResponseHeaderKey = cfg.TraceIDHeader
|
||||
if c.TraceIDResponseHeaderKey == "" {
|
||||
c.TraceIDResponseHeaderKey = DefaultTraceIDResponseHeaderKey
|
||||
}
|
||||
|
||||
c.TraceSampledResponseHeaderKey = cfg.TraceSampledHeader
|
||||
if c.TraceSampledResponseHeaderKey == "" {
|
||||
c.TraceSampledResponseHeaderKey = DefaultTraceSampledResponseHeaderKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithPublicEndpoint is used for marking every endpoint as public endpoint.
|
||||
// This means if the incoming request has span context, it won't be used as
|
||||
// parent span by the span generated by this middleware, instead the generated
|
||||
// span will be the root span (new trace) and then linked to the span from the
|
||||
// incoming request.
|
||||
//
|
||||
// Let say we have the following scenario:
|
||||
//
|
||||
// 1. We have 2 systems: `SysA` & `SysB`.
|
||||
// 2. `SysA` has the following services: `SvcA.1` & `SvcA.2`.
|
||||
// 3. `SysB` has the following services: `SvcB.1` & `SvcB.2`.
|
||||
// 4. `SvcA.2` is used internally only by `SvcA.1`.
|
||||
// 5. `SvcB.2` is used internally only by `SvcB.1`.
|
||||
// 6. All of these services already instrumented otelchi & using the same collector (e.g Jaeger).
|
||||
// 7. In `SvcA.1` we should set `WithPublicEndpoint()` since it is the entry point (a.k.a "public endpoint") for entering `SysA`.
|
||||
// 8. In `SvcA.2` we should not set `WithPublicEndpoint()` since it is only used internally by `SvcA.1` inside `SysA`.
|
||||
// 9. Point 7 & 8 also applies to both services in `SysB`.
|
||||
//
|
||||
// Now, whenever `SvcA.1` calls `SvcA.2` there will be only a single trace generated. This trace will contain 2 spans: root span from `SvcA.1` & child span from `SvcA.2`.
|
||||
//
|
||||
// But if let say `SvcA.2` calls `SvcB.1`, then there will be 2 traces generated: trace from `SysA` & trace from `SysB`. But in trace generated in `SysB` there will be like a marking that this trace is actually related to trace in `SysA` (a.k.a linked with the trace from `SysA`).
|
||||
func WithPublicEndpoint() Option {
|
||||
return WithPublicEndpointFn(func(r *http.Request) bool { return true })
|
||||
}
|
||||
|
||||
// WithPublicEndpointFn runs with every request, and allows conditionally
|
||||
// configuring the Handler to link the generated span with an incoming span
|
||||
// context.
|
||||
//
|
||||
// If the function return `true` the generated span will be linked with the
|
||||
// incoming span context. Otherwise, the generated span will be set as the
|
||||
// child span of the incoming span context.
|
||||
//
|
||||
// Essentially it has the same functionality as `WithPublicEndpoint` but with
|
||||
// more flexibility.
|
||||
func WithPublicEndpointFn(fn func(r *http.Request) bool) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.PublicEndpointFn = fn
|
||||
})
|
||||
}
|
7
examples/basic/Dockerfile
Normal file
7
examples/basic/Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
|||
FROM golang:1.22-alpine3.20
|
||||
|
||||
WORKDIR /go/src/toastielab.dev/toastie-stuff/otelchi/examples/basic
|
||||
RUN go mod download -x
|
||||
RUN go build -o server
|
||||
|
||||
ENTRYPOINT ["./server"]
|
6
examples/basic/Makefile
Normal file
6
examples/basic/Makefile
Normal file
|
@ -0,0 +1,6 @@
|
|||
run:
|
||||
-make down
|
||||
docker-compose up --build
|
||||
|
||||
down:
|
||||
docker-compose down --remove-orphans
|
25
examples/basic/README.md
Normal file
25
examples/basic/README.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# go-chi/chi instrumentation example
|
||||
|
||||
An HTTP server using go-chi/chi and instrumentation. The server has a `/users/{id:[0-9]+}` endpoint. The server generates span information to `stdout`.
|
||||
|
||||
These instructions expect you to have [docker-compose](https://docs.docker.com/compose/) installed.
|
||||
|
||||
Bring up the `mux-server` and `mux-client` services to run the
|
||||
example:
|
||||
|
||||
```sh
|
||||
docker-compose up --detach mux-server mux-client
|
||||
```
|
||||
|
||||
The `mux-client` service sends just one HTTP request to the `mux-server`
|
||||
and then exits. View the span generated by the `mux-server` in the logs:
|
||||
|
||||
```sh
|
||||
docker-compose logs mux-server
|
||||
```
|
||||
|
||||
Shut down the services when you are finished with the example:
|
||||
|
||||
```sh
|
||||
docker-compose down
|
||||
```
|
18
examples/basic/docker-compose.yml
Normal file
18
examples/basic/docker-compose.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
version: "2.4"
|
||||
services:
|
||||
mux-client:
|
||||
image: curlimages/curl:7.84.0
|
||||
command: "curl -XGET http://mux-server:8080/users/123"
|
||||
depends_on:
|
||||
- mux-server
|
||||
mux-server:
|
||||
build:
|
||||
# we set the build context to toastielab.dev/toastie-stuff/otelchi because we use relative
|
||||
# path in go.mod for otelchi library
|
||||
context: ../../
|
||||
|
||||
# since we are on toastielab.dev/toastie-stuff/otelchi, the dockerfile location would be
|
||||
# in ./examples/basic/Dockerfile
|
||||
dockerfile: ./examples/basic/Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
25
examples/basic/go.mod
Normal file
25
examples/basic/go.mod
Normal file
|
@ -0,0 +1,25 @@
|
|||
module toastielab.dev/toastie-stuff/otelchi/examples/basic
|
||||
|
||||
go 1.22.0
|
||||
|
||||
replace toastielab.dev/toastie-stuff/otelchi => ../../
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
toastielab.dev/toastie-stuff/otelchi v0.10.0
|
||||
go.opentelemetry.io/otel v1.30.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.30.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.30.0
|
||||
go.opentelemetry.io/otel/sdk v1.30.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.30.0
|
||||
go.opentelemetry.io/otel/trace v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
)
|
15
examples/basic/go.sum
Normal file
15
examples/basic/go.sum
Normal file
|
@ -0,0 +1,15 @@
|
|||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/riandyrn/otelchi v0.10.0/go.mod h1:zBaX2FavWMlsvq4GqHit+QXxF1c5wIMZZFaYyW4+7FA=
|
||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.30.0/go.mod h1:bxiX8eUeKoAEQmbq/ecUT8UqZwCjZW52yJrXJUSozsk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.30.0/go.mod h1:ljkUDtAMdleoi9tIG1R6dJUpVwDcYjw3J2Q6Q/SuiC0=
|
||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y=
|
||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
99
examples/basic/main.go
Normal file
99
examples/basic/main.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
"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.20.0"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
|
||||
"toastielab.dev/toastie-stuff/otelchi"
|
||||
)
|
||||
|
||||
var tracer oteltrace.Tracer
|
||||
|
||||
func main() {
|
||||
// initialize trace provider
|
||||
tp, mp := initOtelProviders()
|
||||
defer func() {
|
||||
if err := tp.Shutdown(context.Background()); err != nil {
|
||||
log.Printf("Error shutting down tracer provider: %v", err)
|
||||
}
|
||||
if err := mp.Shutdown(context.Background()); err != nil {
|
||||
log.Printf("Error shutting down meter provider: %v", err)
|
||||
}
|
||||
}()
|
||||
// set global tracer provider & text propagators
|
||||
otel.SetTracerProvider(tp)
|
||||
otel.SetMeterProvider(mp)
|
||||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
|
||||
// initialize tracer
|
||||
tracer = otel.Tracer("mux-server")
|
||||
|
||||
// define router
|
||||
r := chi.NewRouter()
|
||||
r.Use(otelchi.Middleware("my-server", otelchi.WithChiRoutes(r)))
|
||||
r.HandleFunc("/users/{id:[0-9]+}", func(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
name := getUser(r.Context(), id)
|
||||
reply := fmt.Sprintf("user %s (id %s)\n", name, id)
|
||||
w.Write(([]byte)(reply))
|
||||
})
|
||||
|
||||
// serve router
|
||||
_ = http.ListenAndServe(":8080", r)
|
||||
}
|
||||
|
||||
func initOtelProviders() (*sdktrace.TracerProvider, *sdkmetric.MeterProvider) {
|
||||
traceExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
metricExporter, err := stdoutmetric.New(stdoutmetric.WithPrettyPrint())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
res, err := resource.New(
|
||||
context.Background(),
|
||||
resource.WithAttributes(
|
||||
// the service name used to display traces in backends
|
||||
semconv.ServiceNameKey.String("mux-server"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to initialize resource due: %v", err)
|
||||
}
|
||||
return sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||
sdktrace.WithBatcher(traceExporter),
|
||||
sdktrace.WithResource(res),
|
||||
), sdkmetric.NewMeterProvider(
|
||||
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter,
|
||||
sdkmetric.WithInterval(time.Second*30),
|
||||
)),
|
||||
sdkmetric.WithResource(res),
|
||||
)
|
||||
}
|
||||
|
||||
func getUser(ctx context.Context, id string) string {
|
||||
_, span := tracer.Start(ctx, "getUser", oteltrace.WithAttributes(attribute.String("id", id)))
|
||||
defer span.End()
|
||||
if id == "123" {
|
||||
return "otelchi tester"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
6
examples/multi-services/Makefile
Normal file
6
examples/multi-services/Makefile
Normal file
|
@ -0,0 +1,6 @@
|
|||
run:
|
||||
-make down
|
||||
docker-compose up --build
|
||||
|
||||
down:
|
||||
docker-compose down --remove-orphans
|
33
examples/multi-services/README.md
Normal file
33
examples/multi-services/README.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Multi Services Example
|
||||
|
||||
This is a simple example of how instrumenting multiple services.
|
||||
|
||||
There are 2 services in this example:
|
||||
|
||||
- `front-svc` => the front service receiving requests from the client
|
||||
- `back-svc` => the service that is being called by `front-svc` (hence named `back`)
|
||||
|
||||

|
||||
|
||||
All traces will be collected in [Jaeger](https://www.jaegertracing.io/).
|
||||
|
||||
## How to Run
|
||||
|
||||
Make sure to have Docker & Docker Compose installed in your system. After that run this command:
|
||||
|
||||
```
|
||||
> make run
|
||||
```
|
||||
|
||||
If the command runs successfully (it will take a moment), you will see something like this in the terminal:
|
||||
|
||||
```
|
||||
back-svc_1 | 2022/07/23 01:49:29 back service is listening on :8091
|
||||
front-svc_1 | 2022/07/23 01:49:26 front service is listening on :8090
|
||||
...
|
||||
multi-services_client_1 exited with code 0
|
||||
```
|
||||
|
||||
Open your browser and access `http://localhost:16686` to access the Jaeger UI.
|
||||
|
||||
You should see some traces available already in the UI.
|
4
examples/multi-services/architecture.svg
Normal file
4
examples/multi-services/architecture.svg
Normal file
File diff suppressed because one or more lines are too long
After (image error) Size: 7.6 KiB |
13
examples/multi-services/back-svc/Dockerfile
Normal file
13
examples/multi-services/back-svc/Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
FROM golang:1.22-alpine3.20
|
||||
WORKDIR /go/src/toastielab.dev/toastie-stuff/otelchi
|
||||
|
||||
RUN apk --no-cache add curl
|
||||
COPY . .
|
||||
|
||||
WORKDIR /go/src/toastielab.dev/toastie-stuff/otelchi/examples/multi-services
|
||||
RUN go mod download -x
|
||||
|
||||
WORKDIR /go/src/toastielab.dev/toastie-stuff/otelchi/examples/multi-services/back-svc
|
||||
RUN go build -o server
|
||||
|
||||
ENTRYPOINT ["./server"]
|
54
examples/multi-services/back-svc/main.go
Normal file
54
examples/multi-services/back-svc/main.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"toastielab.dev/toastie-stuff/otelchi"
|
||||
"toastielab.dev/toastie-stuff/otelchi/examples/multi-services/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
addr = ":8091"
|
||||
serviceName = "back-svc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// init tracer provider
|
||||
tracer, err := utils.NewTracer(serviceName)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to initialize tracer provider due: %v", err)
|
||||
}
|
||||
|
||||
if err = utils.NewMeter(serviceName); err != nil {
|
||||
log.Fatalf("unable to initialize meter provider due: %v", err)
|
||||
}
|
||||
|
||||
// define router
|
||||
r := chi.NewRouter()
|
||||
r.Use(otelchi.Middleware(serviceName, otelchi.WithChiRoutes(r)))
|
||||
r.Get("/", utils.HealthCheckHandler)
|
||||
r.Get("/name", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(generateName(r.Context(), tracer)))
|
||||
})
|
||||
log.Printf("back service is listening on %v", addr)
|
||||
err = http.ListenAndServe(addr, r)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to execute server due: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateName(ctx context.Context, tracer trace.Tracer) string {
|
||||
_, span := tracer.Start(ctx, "generateName")
|
||||
defer span.End()
|
||||
|
||||
rndNum := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(100000)
|
||||
return fmt.Sprintf("user_%v", rndNum)
|
||||
}
|
13
examples/multi-services/front-svc/Dockerfile
Normal file
13
examples/multi-services/front-svc/Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
FROM golang:1.22-alpine3.20
|
||||
WORKDIR /go/src/toastielab.dev/toastie-stuff/otelchi
|
||||
|
||||
RUN apk --no-cache add curl
|
||||
COPY . .
|
||||
|
||||
WORKDIR /go/src/toastielab.dev/toastie-stuff/otelchi/examples/multi-services
|
||||
RUN go mod download -x
|
||||
|
||||
WORKDIR /go/src/toastielab.dev/toastie-stuff/otelchi/examples/multi-services/front-svc
|
||||
RUN go build -o server
|
||||
|
||||
ENTRYPOINT ["./server"]
|
84
examples/multi-services/front-svc/main.go
Normal file
84
examples/multi-services/front-svc/main.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"toastielab.dev/toastie-stuff/otelchi"
|
||||
"toastielab.dev/toastie-stuff/otelchi/examples/multi-services/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
envKeyBackServiceURL = "BACK_SERVICE_URL"
|
||||
addr = ":8090"
|
||||
serviceName = "front-svc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// initialize tracer
|
||||
tracer, err := utils.NewTracer(serviceName)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to initialize tracer due: %v", err)
|
||||
}
|
||||
|
||||
if err = utils.NewMeter(serviceName); err != nil {
|
||||
log.Fatalf("unable to initialize meter provider due: %v", err)
|
||||
}
|
||||
|
||||
// define router
|
||||
r := chi.NewRouter()
|
||||
r.Use(otelchi.Middleware(serviceName, otelchi.WithChiRoutes(r)))
|
||||
r.Get("/", utils.HealthCheckHandler)
|
||||
r.Get("/greet", func(w http.ResponseWriter, r *http.Request) {
|
||||
name, err := getRandomName(r.Context(), tracer)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(fmt.Sprintf("Hello, %s!", name)))
|
||||
})
|
||||
// execute server
|
||||
log.Printf("front service is listening on %v", addr)
|
||||
err = http.ListenAndServe(addr, r)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to execute server due: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getRandomName(ctx context.Context, tracer trace.Tracer) (string, error) {
|
||||
// start span
|
||||
ctx, span := tracer.Start(ctx, "getRandomName")
|
||||
defer span.End()
|
||||
|
||||
// call back service, notice that here we call the service using instrumented
|
||||
// http client
|
||||
resp, err := otelhttp.Get(ctx, os.Getenv(envKeyBackServiceURL)+"/name")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to execute http request due: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// read response body
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to read response data due: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
36
examples/multi-services/go.mod
Normal file
36
examples/multi-services/go.mod
Normal file
|
@ -0,0 +1,36 @@
|
|||
module toastielab.dev/toastie-stuff/otelchi/examples/multi-services
|
||||
|
||||
go 1.22.0
|
||||
|
||||
replace toastielab.dev/toastie-stuff/otelchi => ../../
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
toastielab.dev/toastie-stuff/otelchi v0.10.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0
|
||||
go.opentelemetry.io/otel v1.30.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.30.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0
|
||||
go.opentelemetry.io/otel/sdk v1.30.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.30.0
|
||||
go.opentelemetry.io/otel/trace v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.66.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
)
|
25
examples/multi-services/go.sum
Normal file
25
examples/multi-services/go.sum
Normal file
|
@ -0,0 +1,25 @@
|
|||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.30.0/go.mod h1:qqN/uFdpeitTvm+JDqqnjm517pmQRYxTORbETHq5tOc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
|
||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y=
|
||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
7
examples/multi-services/utils/healthcheck.go
Normal file
7
examples/multi-services/utils/healthcheck.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package utils
|
||||
|
||||
import "net/http"
|
||||
|
||||
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("It's working!"))
|
||||
}
|
39
examples/multi-services/utils/meter.go
Normal file
39
examples/multi-services/utils/meter.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||
)
|
||||
|
||||
func NewMeter(svcName string) error {
|
||||
// create otlp exporter, notice that here we are using insecure option
|
||||
// because we just want to export the trace locally, also notice that
|
||||
// here we don't set any endpoint because by default the otel will load
|
||||
// the endpoint from the environment variable `OTEL_EXPORTER_OTLP_ENDPOINT`
|
||||
exporter, err := otlpmetrichttp.New(
|
||||
context.Background(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize exporter due: %w", err)
|
||||
}
|
||||
// initialize tracer provider
|
||||
tp := sdkmetric.NewMeterProvider(
|
||||
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter, sdkmetric.WithInterval(30*time.Second))),
|
||||
sdkmetric.WithResource(resource.NewWithAttributes(
|
||||
semconv.SchemaURL,
|
||||
semconv.ServiceNameKey.String(svcName),
|
||||
)),
|
||||
)
|
||||
// set tracer provider and propagator properly, this is to ensure all
|
||||
// instrumentation library could run well
|
||||
otel.SetMeterProvider(tp)
|
||||
|
||||
return nil
|
||||
}
|
45
examples/multi-services/utils/tracer.go
Normal file
45
examples/multi-services/utils/tracer.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func NewTracer(svcName string) (trace.Tracer, error) {
|
||||
// create otlp exporter, notice that here we are using insecure option
|
||||
// because we just want to export the trace locally, also notice that
|
||||
// here we don't set any endpoint because by default the otel will load
|
||||
// the endpoint from the environment variable `OTEL_EXPORTER_OTLP_ENDPOINT`
|
||||
exporter, err := otlptrace.New(
|
||||
context.Background(),
|
||||
otlptracehttp.NewClient(otlptracehttp.WithInsecure()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize exporter due: %w", err)
|
||||
}
|
||||
// initialize tracer provider
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||
sdktrace.WithBatcher(exporter),
|
||||
sdktrace.WithResource(resource.NewWithAttributes(
|
||||
semconv.SchemaURL,
|
||||
semconv.ServiceNameKey.String(svcName),
|
||||
)),
|
||||
)
|
||||
// set tracer provider and propagator properly, this is to ensure all
|
||||
// instrumentation library could run well
|
||||
otel.SetTracerProvider(tp)
|
||||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
|
||||
|
||||
// returns tracer
|
||||
return otel.Tracer(svcName), nil
|
||||
}
|
23
go.mod
Normal file
23
go.mod
Normal file
|
@ -0,0 +1,23 @@
|
|||
module toastielab.dev/toastie-stuff/otelchi
|
||||
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/felixge/httpsnoop v1.0.4
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.opentelemetry.io/otel v1.30.0
|
||||
go.opentelemetry.io/otel/metric v1.30.0
|
||||
go.opentelemetry.io/otel/sdk v1.30.0
|
||||
go.opentelemetry.io/otel/trace v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
16
go.sum
Normal file
16
go.sum
Normal file
|
@ -0,0 +1,16 @@
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
88
metrics.go
Normal file
88
metrics.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package otelchi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
otelmetric "go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
var (
|
||||
serviceKey = attribute.Key("service")
|
||||
idKey = attribute.Key("id")
|
||||
methodKey = attribute.Key("method")
|
||||
codeKey = attribute.Key("code")
|
||||
)
|
||||
|
||||
type httpReqProperties struct {
|
||||
Service string
|
||||
ID string
|
||||
Method string
|
||||
Code int
|
||||
}
|
||||
|
||||
func newMetricsRecorder(meter otelmetric.Meter) *metricsRecorder {
|
||||
httpRequestDurHistogram, err := meter.Int64Histogram("request_duration_seconds")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create request_duration_seconds histogram: %v", err))
|
||||
}
|
||||
|
||||
httpResponseSizeHistogram, err := meter.Int64Histogram("response_size_bytes")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create response_size_bytes histogram: %v", err))
|
||||
}
|
||||
|
||||
httpRequestsInflight, err := meter.Int64UpDownCounter("requests_inflight")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create requests_inflight counter: %v", err))
|
||||
}
|
||||
|
||||
return &metricsRecorder{
|
||||
httpRequestDurHistogram: httpRequestDurHistogram,
|
||||
httpResponseSizeHistogram: httpResponseSizeHistogram,
|
||||
httpRequestsInflight: httpRequestsInflight,
|
||||
}
|
||||
}
|
||||
|
||||
type metricsRecorder struct {
|
||||
httpRequestDurHistogram otelmetric.Int64Histogram
|
||||
httpResponseSizeHistogram otelmetric.Int64Histogram
|
||||
httpRequestsInflight otelmetric.Int64UpDownCounter
|
||||
}
|
||||
|
||||
func (r *metricsRecorder) RecordRequestDuration(ctx context.Context, p httpReqProperties, duration time.Duration) {
|
||||
r.httpRequestDurHistogram.Record(ctx,
|
||||
int64(duration.Seconds()),
|
||||
otelmetric.WithAttributes(
|
||||
serviceKey.String(p.Service),
|
||||
idKey.String(p.ID),
|
||||
methodKey.String(p.Method),
|
||||
codeKey.Int(p.Code),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *metricsRecorder) RecordResponseSize(ctx context.Context, p httpReqProperties, size int64) {
|
||||
r.httpResponseSizeHistogram.Record(ctx,
|
||||
size,
|
||||
otelmetric.WithAttributes(
|
||||
serviceKey.String(p.Service),
|
||||
idKey.String(p.ID),
|
||||
methodKey.String(p.Method),
|
||||
codeKey.Int(p.Code),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *metricsRecorder) RecordRequestsInflight(ctx context.Context, p httpReqProperties, count int64) {
|
||||
r.httpRequestsInflight.Add(ctx,
|
||||
count,
|
||||
otelmetric.WithAttributes(
|
||||
serviceKey.String(p.Service),
|
||||
idKey.String(p.ID),
|
||||
methodKey.String(p.Method),
|
||||
),
|
||||
)
|
||||
}
|
270
middleware.go
Normal file
270
middleware.go
Normal file
|
@ -0,0 +1,270 @@
|
|||
package otelchi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/felixge/httpsnoop"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
|
||||
otelmetric "go.opentelemetry.io/otel/metric"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.20.0/httpconv"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
tracerName = "toastielab.dev/toastie-stuff/otelchi"
|
||||
)
|
||||
|
||||
// Middleware sets up a handler to start tracing the incoming
|
||||
// requests. The serverName parameter should describe the name of the
|
||||
// (virtual) server handling the request.
|
||||
func Middleware(serverName string, opts ...Option) func(http.Handler) http.Handler {
|
||||
cfg := config{}
|
||||
for _, opt := range opts {
|
||||
opt.apply(&cfg)
|
||||
}
|
||||
|
||||
if cfg.TracerProvider == nil {
|
||||
cfg.TracerProvider = otel.GetTracerProvider()
|
||||
}
|
||||
tracer := cfg.TracerProvider.Tracer(
|
||||
tracerName,
|
||||
oteltrace.WithInstrumentationVersion(Version()),
|
||||
)
|
||||
|
||||
if cfg.MeterProvider == nil {
|
||||
cfg.MeterProvider = otel.GetMeterProvider()
|
||||
}
|
||||
meter := cfg.MeterProvider.Meter(
|
||||
tracerName,
|
||||
otelmetric.WithInstrumentationVersion(Version()),
|
||||
)
|
||||
|
||||
if cfg.Propagators == nil {
|
||||
cfg.Propagators = otel.GetTextMapPropagator()
|
||||
}
|
||||
|
||||
return func(handler http.Handler) http.Handler {
|
||||
return &otelware{
|
||||
serverName: serverName,
|
||||
tracer: tracer,
|
||||
meter: meter,
|
||||
recorder: newMetricsRecorder(meter),
|
||||
propagators: cfg.Propagators,
|
||||
handler: handler,
|
||||
chiRoutes: cfg.ChiRoutes,
|
||||
reqMethodInSpanName: cfg.RequestMethodInSpanName,
|
||||
filters: cfg.Filters,
|
||||
traceIDResponseHeaderKey: cfg.TraceIDResponseHeaderKey,
|
||||
traceSampledResponseHeaderKey: cfg.TraceSampledResponseHeaderKey,
|
||||
publicEndpointFn: cfg.PublicEndpointFn,
|
||||
disableMeasureInflight: cfg.DisableMeasureInflight,
|
||||
disableMeasureSize: cfg.DisableMeasureSize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type otelware struct {
|
||||
serverName string
|
||||
tracer oteltrace.Tracer
|
||||
meter otelmetric.Meter
|
||||
recorder *metricsRecorder
|
||||
propagators propagation.TextMapPropagator
|
||||
handler http.Handler
|
||||
chiRoutes chi.Routes
|
||||
reqMethodInSpanName bool
|
||||
filters []Filter
|
||||
traceIDResponseHeaderKey string
|
||||
traceSampledResponseHeaderKey string
|
||||
publicEndpointFn func(r *http.Request) bool
|
||||
|
||||
disableMeasureInflight bool
|
||||
disableMeasureSize bool
|
||||
}
|
||||
|
||||
type recordingResponseWriter struct {
|
||||
writer http.ResponseWriter
|
||||
written bool
|
||||
writtenBytes int64
|
||||
status int
|
||||
}
|
||||
|
||||
var rrwPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &recordingResponseWriter{}
|
||||
},
|
||||
}
|
||||
|
||||
func getRRW(writer http.ResponseWriter) *recordingResponseWriter {
|
||||
rrw := rrwPool.Get().(*recordingResponseWriter)
|
||||
rrw.written = false
|
||||
rrw.writtenBytes = 0
|
||||
rrw.status = http.StatusOK
|
||||
rrw.writer = httpsnoop.Wrap(writer, httpsnoop.Hooks{
|
||||
Write: func(next httpsnoop.WriteFunc) httpsnoop.WriteFunc {
|
||||
return func(b []byte) (int, error) {
|
||||
if !rrw.written {
|
||||
rrw.written = true
|
||||
rrw.writtenBytes += int64(len(b))
|
||||
}
|
||||
return next(b)
|
||||
}
|
||||
},
|
||||
WriteHeader: func(next httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
|
||||
return func(statusCode int) {
|
||||
if !rrw.written {
|
||||
rrw.written = true
|
||||
rrw.status = statusCode
|
||||
}
|
||||
next(statusCode)
|
||||
}
|
||||
},
|
||||
})
|
||||
return rrw
|
||||
}
|
||||
|
||||
func putRRW(rrw *recordingResponseWriter) {
|
||||
rrw.writer = nil
|
||||
rrwPool.Put(rrw)
|
||||
}
|
||||
|
||||
// ServeHTTP implements the http.Handler interface. It does the actual
|
||||
// tracing of the request.
|
||||
func (ow *otelware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// go through all filters if any
|
||||
for _, filter := range ow.filters {
|
||||
// if there is a filter that returns false, we skip tracing
|
||||
// and execute next handler
|
||||
if !filter(r) {
|
||||
ow.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// extract tracing header using propagator
|
||||
ctx := ow.propagators.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
|
||||
// create span, based on specification, we need to set already known attributes
|
||||
// when creating the span, the only thing missing here is HTTP route pattern since
|
||||
// in go-chi/chi route pattern could only be extracted once the request is executed
|
||||
// check here for details:
|
||||
//
|
||||
// https://github.com/go-chi/chi/issues/150#issuecomment-278850733
|
||||
//
|
||||
// if we have access to chi routes, we could extract the route pattern beforehand.
|
||||
spanName := ""
|
||||
routePattern := ""
|
||||
spanAttributes := httpconv.ServerRequest(ow.serverName, r)
|
||||
|
||||
if ow.chiRoutes != nil {
|
||||
rctx := chi.NewRouteContext()
|
||||
if ow.chiRoutes.Match(rctx, r.Method, r.URL.Path) {
|
||||
routePattern = rctx.RoutePattern()
|
||||
spanName = addPrefixToSpanName(ow.reqMethodInSpanName, r.Method, routePattern)
|
||||
spanAttributes = append(spanAttributes, semconv.HTTPRoute(routePattern))
|
||||
}
|
||||
}
|
||||
|
||||
// define span start options
|
||||
spanOpts := []oteltrace.SpanStartOption{
|
||||
oteltrace.WithAttributes(spanAttributes...),
|
||||
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
|
||||
}
|
||||
|
||||
if ow.publicEndpointFn != nil && ow.publicEndpointFn(r) {
|
||||
// mark span as the root span
|
||||
spanOpts = append(spanOpts, oteltrace.WithNewRoot())
|
||||
|
||||
// linking incoming span context to the root span, we need to
|
||||
// ensure if the incoming span context is valid (because it is
|
||||
// possible for us to receive invalid span context due to various
|
||||
// reason such as bug or context propagation error) and it is
|
||||
// coming from another service (remote) before linking it to the
|
||||
// root span
|
||||
spanCtx := oteltrace.SpanContextFromContext(ctx)
|
||||
if spanCtx.IsValid() && spanCtx.IsRemote() {
|
||||
spanOpts = append(
|
||||
spanOpts,
|
||||
oteltrace.WithLinks(oteltrace.Link{
|
||||
SpanContext: spanCtx,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
props := httpReqProperties{
|
||||
Service: ow.serverName,
|
||||
ID: routePattern,
|
||||
Method: r.Method,
|
||||
}
|
||||
if routePattern == "" {
|
||||
props.ID = r.URL.Path
|
||||
}
|
||||
|
||||
if !ow.disableMeasureInflight {
|
||||
ow.recorder.RecordRequestsInflight(ctx, props, 1)
|
||||
defer ow.recorder.RecordRequestsInflight(ctx, props, -1)
|
||||
}
|
||||
|
||||
// start span
|
||||
ctx, span := ow.tracer.Start(ctx, spanName, spanOpts...)
|
||||
defer span.End()
|
||||
|
||||
// put trace_id to response header only when `WithTraceIDResponseHeader` is used
|
||||
if len(ow.traceIDResponseHeaderKey) > 0 && span.SpanContext().HasTraceID() {
|
||||
w.Header().Add(ow.traceIDResponseHeaderKey, span.SpanContext().TraceID().String())
|
||||
w.Header().Add(ow.traceSampledResponseHeaderKey, strconv.FormatBool(span.SpanContext().IsSampled()))
|
||||
}
|
||||
|
||||
// get recording response writer
|
||||
rrw := getRRW(w)
|
||||
defer putRRW(rrw)
|
||||
|
||||
// execute next http handler
|
||||
r = r.WithContext(ctx)
|
||||
start := time.Now()
|
||||
ow.handler.ServeHTTP(rrw.writer, r)
|
||||
duration := time.Since(start)
|
||||
|
||||
props.Code = rrw.status
|
||||
ow.recorder.RecordRequestDuration(ctx, props, duration)
|
||||
|
||||
if !ow.disableMeasureSize {
|
||||
ow.recorder.RecordResponseSize(ctx, props, rrw.writtenBytes)
|
||||
}
|
||||
|
||||
// set span name & http route attribute if route pattern cannot be determined
|
||||
// during span creation
|
||||
if len(routePattern) == 0 {
|
||||
routePattern = chi.RouteContext(r.Context()).RoutePattern()
|
||||
span.SetAttributes(semconv.HTTPRoute(routePattern))
|
||||
|
||||
spanName = addPrefixToSpanName(ow.reqMethodInSpanName, r.Method, routePattern)
|
||||
span.SetName(spanName)
|
||||
}
|
||||
|
||||
// set status code attribute
|
||||
span.SetAttributes(semconv.HTTPStatusCode(rrw.status))
|
||||
|
||||
// set span status
|
||||
span.SetStatus(httpconv.ServerStatus(rrw.status))
|
||||
}
|
||||
|
||||
func addPrefixToSpanName(shouldAdd bool, prefix, spanName string) string {
|
||||
// in chi v5.0.8, the root route will be returned has an empty string
|
||||
// (see https://github.com/go-chi/chi/blob/v5.0.8/context.go#L126)
|
||||
if spanName == "" {
|
||||
spanName = "/"
|
||||
}
|
||||
|
||||
if shouldAdd && len(spanName) > 0 {
|
||||
spanName = prefix + " " + spanName
|
||||
}
|
||||
return spanName
|
||||
}
|
150
test/cases/middleware_test.go
Normal file
150
test/cases/middleware_test.go
Normal file
|
@ -0,0 +1,150 @@
|
|||
package otelchi_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"toastielab.dev/toastie-stuff/otelchi"
|
||||
)
|
||||
|
||||
var sc = trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: [16]byte{1},
|
||||
SpanID: [8]byte{1},
|
||||
Remote: true,
|
||||
TraceFlags: trace.FlagsSampled,
|
||||
})
|
||||
|
||||
func TestPassthroughSpanFromGlobalTracer(t *testing.T) {
|
||||
var called bool
|
||||
router := chi.NewRouter()
|
||||
router.Use(otelchi.Middleware("foobar"))
|
||||
// The default global TracerProvider provides "pass through" spans for any
|
||||
// span context in the incoming request context.
|
||||
router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
got := trace.SpanFromContext(r.Context()).SpanContext()
|
||||
assert.Equal(t, sc, got)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
r := httptest.NewRequest("GET", "/user/123", nil)
|
||||
r = r.WithContext(trace.ContextWithRemoteSpanContext(context.Background(), sc))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, r)
|
||||
assert.True(t, called, "failed to run test")
|
||||
}
|
||||
|
||||
func TestPropagationWithGlobalPropagators(t *testing.T) {
|
||||
defer func(p propagation.TextMapPropagator) {
|
||||
otel.SetTextMapPropagator(p)
|
||||
}(otel.GetTextMapPropagator())
|
||||
|
||||
prop := propagation.TraceContext{}
|
||||
otel.SetTextMapPropagator(prop)
|
||||
|
||||
r := httptest.NewRequest("GET", "/user/123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
ctx := trace.ContextWithRemoteSpanContext(context.Background(), sc)
|
||||
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(r.Header))
|
||||
|
||||
var called bool
|
||||
router := chi.NewRouter()
|
||||
router.Use(otelchi.Middleware("foobar"))
|
||||
router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
assert.Equal(t, sc, span.SpanContext())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
router.ServeHTTP(w, r)
|
||||
assert.True(t, called, "failed to run test")
|
||||
}
|
||||
|
||||
func TestPropagationWithCustomPropagators(t *testing.T) {
|
||||
prop := propagation.TraceContext{}
|
||||
|
||||
r := httptest.NewRequest("GET", "/user/123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
ctx := trace.ContextWithRemoteSpanContext(context.Background(), sc)
|
||||
prop.Inject(ctx, propagation.HeaderCarrier(r.Header))
|
||||
|
||||
var called bool
|
||||
router := chi.NewRouter()
|
||||
router.Use(otelchi.Middleware("foobar", otelchi.WithPropagators(prop)))
|
||||
router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
assert.Equal(t, sc, span.SpanContext())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
router.ServeHTTP(w, r)
|
||||
assert.True(t, called, "failed to run test")
|
||||
}
|
||||
|
||||
func TestResponseWriterInterfaces(t *testing.T) {
|
||||
// make sure the recordingResponseWriter preserves interfaces implemented by the wrapped writer
|
||||
router := chi.NewRouter()
|
||||
router.Use(otelchi.Middleware("foobar"))
|
||||
router.HandleFunc("/user/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Implements(t, (*http.Hijacker)(nil), w)
|
||||
assert.Implements(t, (*http.Pusher)(nil), w)
|
||||
assert.Implements(t, (*http.Flusher)(nil), w)
|
||||
assert.Implements(t, (*io.ReaderFrom)(nil), w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
r := httptest.NewRequest("GET", "/user/123", nil)
|
||||
w := &testResponseWriter{
|
||||
writer: httptest.NewRecorder(),
|
||||
}
|
||||
|
||||
router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
type testResponseWriter struct {
|
||||
writer http.ResponseWriter
|
||||
}
|
||||
|
||||
func (rw *testResponseWriter) Header() http.Header {
|
||||
return rw.writer.Header()
|
||||
}
|
||||
func (rw *testResponseWriter) Write(b []byte) (int, error) {
|
||||
return rw.writer.Write(b)
|
||||
}
|
||||
func (rw *testResponseWriter) WriteHeader(statusCode int) {
|
||||
rw.writer.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
// implement Hijacker
|
||||
func (rw *testResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// implement Pusher
|
||||
func (rw *testResponseWriter) Push(target string, opts *http.PushOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// implement Flusher
|
||||
func (rw *testResponseWriter) Flush() {
|
||||
}
|
||||
|
||||
// implement io.ReaderFrom
|
||||
func (rw *testResponseWriter) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
return 0, nil
|
||||
}
|
782
test/cases/sdk_test.go
Normal file
782
test/cases/sdk_test.go
Normal file
|
@ -0,0 +1,782 @@
|
|||
package otelchi_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"toastielab.dev/toastie-stuff/otelchi"
|
||||
)
|
||||
|
||||
func TestSDKIntegration(t *testing.T) {
|
||||
// prepare router and span recorder
|
||||
router, sr := newSDKTestRouter("foobar", false)
|
||||
|
||||
// define routes
|
||||
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
||||
router.HandleFunc("/book/{title}", ok)
|
||||
|
||||
// execute requests
|
||||
reqs := []*http.Request{
|
||||
httptest.NewRequest("GET", "/user/123", nil),
|
||||
httptest.NewRequest("GET", "/book/foo", nil),
|
||||
}
|
||||
executeRequests(router, reqs)
|
||||
|
||||
// get recorded spans
|
||||
recordedSpans := sr.Ended()
|
||||
|
||||
// ensure that we have 2 recorded spans
|
||||
require.Len(t, recordedSpans, len(reqs))
|
||||
|
||||
// ensure span values
|
||||
checkSpans(t, recordedSpans, []spanValueCheck{
|
||||
{
|
||||
Name: "/user/{id:[0-9]+}",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/user/{id:[0-9]+}",
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "/book/{title}",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/book/{title}",
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSDKIntegrationWithFilter(t *testing.T) {
|
||||
// prepare test cases
|
||||
serviceName := "foobar"
|
||||
testCases := []struct {
|
||||
Name string
|
||||
FilterFn []otelchi.Filter
|
||||
LenSpans int
|
||||
ExpectedRouteNames []string
|
||||
}{
|
||||
{
|
||||
Name: "One WithFilter",
|
||||
FilterFn: []otelchi.Filter{
|
||||
func(r *http.Request) bool {
|
||||
return r.URL.Path != "/live" && r.URL.Path != "/ready"
|
||||
},
|
||||
},
|
||||
LenSpans: 2,
|
||||
ExpectedRouteNames: []string{"/user/{id:[0-9]+}", "/book/{title}"},
|
||||
},
|
||||
{
|
||||
Name: "Multiple WithFilter",
|
||||
FilterFn: []otelchi.Filter{
|
||||
func(r *http.Request) bool {
|
||||
return r.URL.Path != "/ready"
|
||||
},
|
||||
func(r *http.Request) bool {
|
||||
return r.URL.Path != "/live"
|
||||
},
|
||||
},
|
||||
LenSpans: 2,
|
||||
ExpectedRouteNames: []string{"/user/{id:[0-9]+}", "/book/{title}"},
|
||||
},
|
||||
{
|
||||
Name: "All Routes are traced",
|
||||
FilterFn: []otelchi.Filter{
|
||||
func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
LenSpans: 4,
|
||||
ExpectedRouteNames: []string{"/user/{id:[0-9]+}", "/book/{title}", "/live", "/ready"},
|
||||
},
|
||||
{
|
||||
Name: "All Routes are not traced",
|
||||
FilterFn: []otelchi.Filter{
|
||||
func(r *http.Request) bool {
|
||||
return false
|
||||
},
|
||||
},
|
||||
LenSpans: 0,
|
||||
ExpectedRouteNames: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
// execute test cases
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
// prepare router and span recorder
|
||||
filters := []otelchi.Option{}
|
||||
for _, filter := range testCase.FilterFn {
|
||||
filters = append(filters, otelchi.WithFilter(filter))
|
||||
}
|
||||
router, sr := newSDKTestRouter(serviceName, false, filters...)
|
||||
|
||||
// define router
|
||||
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
||||
router.HandleFunc("/book/{title}", ok)
|
||||
router.HandleFunc("/health", ok)
|
||||
router.HandleFunc("/live", ok)
|
||||
router.HandleFunc("/ready", ok)
|
||||
|
||||
// execute requests
|
||||
executeRequests(router, []*http.Request{
|
||||
httptest.NewRequest("GET", "/user/123", nil),
|
||||
httptest.NewRequest("GET", "/book/foo", nil),
|
||||
httptest.NewRequest("GET", "/live", nil),
|
||||
httptest.NewRequest("GET", "/ready", nil),
|
||||
})
|
||||
|
||||
// check recorded spans
|
||||
recordedSpans := sr.Ended()
|
||||
require.Len(t, recordedSpans, testCase.LenSpans)
|
||||
|
||||
// ensure span values
|
||||
spanValues := []spanValueCheck{}
|
||||
for _, routeName := range testCase.ExpectedRouteNames {
|
||||
spanValues = append(spanValues, spanValueCheck{
|
||||
Name: routeName,
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
serviceName,
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
routeName,
|
||||
),
|
||||
})
|
||||
}
|
||||
checkSpans(t, recordedSpans, spanValues)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSDKIntegrationWithChiRoutes(t *testing.T) {
|
||||
// define router & span recorder
|
||||
router, sr := newSDKTestRouter("foobar", true)
|
||||
|
||||
// define route
|
||||
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
||||
router.HandleFunc("/book/{title}", ok)
|
||||
|
||||
// execute requests
|
||||
reqs := []*http.Request{
|
||||
httptest.NewRequest("GET", "/user/123", nil),
|
||||
httptest.NewRequest("GET", "/book/foo", nil),
|
||||
}
|
||||
executeRequests(router, reqs)
|
||||
|
||||
// get recorded spans
|
||||
recordedSpans := sr.Ended()
|
||||
|
||||
// ensure that we have 2 recorded spans
|
||||
require.Len(t, recordedSpans, len(reqs))
|
||||
|
||||
// ensure span values
|
||||
checkSpans(t, recordedSpans, []spanValueCheck{
|
||||
{
|
||||
Name: "/user/{id:[0-9]+}",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/user/{id:[0-9]+}",
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "/book/{title}",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/book/{title}",
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSDKIntegrationOverrideSpanName(t *testing.T) {
|
||||
// prepare test router and span recorder
|
||||
router, sr := newSDKTestRouter("foobar", true)
|
||||
|
||||
// define route
|
||||
router.HandleFunc("/user/{id:[0-9]+}", func(w http.ResponseWriter, r *http.Request) {
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
span.SetName("overriden span name")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
router.HandleFunc("/book/{title}", ok)
|
||||
|
||||
// execute requests
|
||||
reqs := []*http.Request{
|
||||
httptest.NewRequest("GET", "/user/123", nil),
|
||||
httptest.NewRequest("GET", "/book/foo", nil),
|
||||
}
|
||||
executeRequests(router, reqs)
|
||||
|
||||
// get recorded spans
|
||||
recordedSpans := sr.Ended()
|
||||
|
||||
// ensure the number of spans is correct
|
||||
require.Len(t, sr.Ended(), len(reqs))
|
||||
|
||||
// check span values
|
||||
checkSpans(t, recordedSpans, []spanValueCheck{
|
||||
{
|
||||
Name: "overriden span name",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/user/{id:[0-9]+}",
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "/book/{title}",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/book/{title}",
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSDKIntegrationWithRequestMethodInSpanName(t *testing.T) {
|
||||
// prepare router & span recorder
|
||||
router, sr := newSDKTestRouter("foobar", true, otelchi.WithRequestMethodInSpanName(true))
|
||||
|
||||
// define handler
|
||||
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
||||
router.HandleFunc("/book/{title}", ok)
|
||||
|
||||
// execute requests
|
||||
reqs := []*http.Request{
|
||||
httptest.NewRequest("GET", "/user/123", nil),
|
||||
httptest.NewRequest("GET", "/book/foo", nil),
|
||||
}
|
||||
executeRequests(router, reqs)
|
||||
|
||||
// get recorded spans & ensure the number is correct
|
||||
recordedSpans := sr.Ended()
|
||||
require.Len(t, sr.Ended(), len(reqs))
|
||||
|
||||
// check span values
|
||||
checkSpans(t, recordedSpans, []spanValueCheck{
|
||||
{
|
||||
Name: "GET /user/{id:[0-9]+}",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/user/{id:[0-9]+}",
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "GET /book/{title}",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/book/{title}",
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSDKIntegrationEmptyHandlerDefaultStatusCode(t *testing.T) {
|
||||
// prepare router and span recorder
|
||||
router, sr := newSDKTestRouter("foobar", false)
|
||||
|
||||
// define routes
|
||||
router.HandleFunc("/user/{id:[0-9]+}", func(w http.ResponseWriter, r *http.Request) {})
|
||||
router.HandleFunc("/book/{title}", func(w http.ResponseWriter, r *http.Request) {})
|
||||
router.HandleFunc("/not-found", http.NotFound)
|
||||
|
||||
// execute requests
|
||||
reqs := []*http.Request{
|
||||
httptest.NewRequest("GET", "/user/123", nil),
|
||||
httptest.NewRequest("GET", "/book/foo", nil),
|
||||
httptest.NewRequest("GET", "/not-found", nil),
|
||||
}
|
||||
executeRequests(router, reqs)
|
||||
|
||||
// get recorded spans
|
||||
recordedSpans := sr.Ended()
|
||||
|
||||
// ensure that we have 3 recorded spans
|
||||
require.Len(t, recordedSpans, len(reqs))
|
||||
|
||||
// ensure span values
|
||||
checkSpans(t, recordedSpans, []spanValueCheck{
|
||||
{
|
||||
Name: "/user/{id:[0-9]+}",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/user/{id:[0-9]+}",
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "/book/{title}",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/book/{title}",
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "/not-found",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusNotFound,
|
||||
"GET",
|
||||
"/not-found",
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSDKIntegrationRootHandler(t *testing.T) {
|
||||
// prepare router and span recorder
|
||||
router, sr := newSDKTestRouter("foobar", true)
|
||||
|
||||
// define routes
|
||||
router.HandleFunc("/", ok)
|
||||
|
||||
// execute requests
|
||||
reqs := []*http.Request{
|
||||
httptest.NewRequest("GET", "/", nil),
|
||||
}
|
||||
executeRequests(router, reqs)
|
||||
|
||||
// get recorded spans
|
||||
recordedSpans := sr.Ended()
|
||||
|
||||
// ensure that we have 1 recorded span
|
||||
require.Len(t, recordedSpans, 1)
|
||||
|
||||
// ensure span values
|
||||
checkSpans(t, recordedSpans, []spanValueCheck{
|
||||
{
|
||||
Name: "/",
|
||||
Kind: trace.SpanKindServer,
|
||||
Attributes: getSemanticAttributes(
|
||||
"foobar",
|
||||
http.StatusOK,
|
||||
"GET",
|
||||
"/",
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSDKIntegrationWithTraceIDResponseHeader(t *testing.T) {
|
||||
// prepare both sampled & non-sampled span context
|
||||
spanCtxSampled := trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: [16]byte{1},
|
||||
SpanID: [8]byte{1},
|
||||
Remote: true,
|
||||
TraceFlags: trace.FlagsSampled,
|
||||
})
|
||||
spanCtxNotSampled := trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: [16]byte{2},
|
||||
SpanID: [8]byte{2},
|
||||
Remote: true,
|
||||
TraceFlags: 0,
|
||||
})
|
||||
|
||||
// define custom header key function
|
||||
customHeaderKeyFunc := func() string {
|
||||
return "X-Custom-Trace-ID"
|
||||
}
|
||||
|
||||
// define test cases
|
||||
testCases := []struct {
|
||||
Name string
|
||||
HeaderKeyFunc func() string
|
||||
SpanContext trace.SpanContext
|
||||
ExpTraceResponseIDKey string
|
||||
ExpTraceResponseSampledKeyVal bool
|
||||
}{
|
||||
{
|
||||
Name: "Default Header Key, Trace Sampled",
|
||||
HeaderKeyFunc: nil,
|
||||
SpanContext: spanCtxSampled,
|
||||
ExpTraceResponseIDKey: otelchi.DefaultTraceIDResponseHeaderKey,
|
||||
ExpTraceResponseSampledKeyVal: true,
|
||||
},
|
||||
{
|
||||
Name: "Default Header Key, Trace Not Sampled",
|
||||
HeaderKeyFunc: nil,
|
||||
SpanContext: spanCtxNotSampled,
|
||||
ExpTraceResponseIDKey: otelchi.DefaultTraceIDResponseHeaderKey,
|
||||
ExpTraceResponseSampledKeyVal: false,
|
||||
},
|
||||
{
|
||||
Name: "Custom Header Key, Trace Sampled",
|
||||
HeaderKeyFunc: customHeaderKeyFunc,
|
||||
SpanContext: spanCtxSampled,
|
||||
ExpTraceResponseIDKey: customHeaderKeyFunc(),
|
||||
ExpTraceResponseSampledKeyVal: true,
|
||||
},
|
||||
{
|
||||
Name: "Custom Header Key, Trace Not Sampled",
|
||||
HeaderKeyFunc: customHeaderKeyFunc,
|
||||
SpanContext: spanCtxNotSampled,
|
||||
ExpTraceResponseIDKey: customHeaderKeyFunc(),
|
||||
ExpTraceResponseSampledKeyVal: false,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
// configure router
|
||||
router := chi.NewRouter()
|
||||
router.Use(
|
||||
otelchi.Middleware(
|
||||
"foobar",
|
||||
otelchi.WithChiRoutes(router),
|
||||
otelchi.WithTraceIDResponseHeader(testCase.HeaderKeyFunc),
|
||||
),
|
||||
)
|
||||
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
||||
router.HandleFunc("/book/{title}", ok)
|
||||
|
||||
// execute requests
|
||||
r0 := httptest.NewRequest("GET", "/user/123", nil)
|
||||
r0 = r0.WithContext(trace.ContextWithRemoteSpanContext(context.Background(), testCase.SpanContext))
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, r0)
|
||||
|
||||
// check response headers
|
||||
require.Equal(t, testCase.SpanContext.TraceID().String(), w.Header().Get(testCase.ExpTraceResponseIDKey))
|
||||
require.Equal(t, fmt.Sprintf("%v", testCase.ExpTraceResponseSampledKeyVal), w.Header().Get(otelchi.DefaultTraceSampledResponseHeaderKey))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDKIntegrationWithTraceResponseHeaders(t *testing.T) {
|
||||
// prepare both sampled & non-sampled span context
|
||||
spanCtxSampled := trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: [16]byte{1},
|
||||
SpanID: [8]byte{1},
|
||||
Remote: true,
|
||||
TraceFlags: trace.FlagsSampled,
|
||||
})
|
||||
spanCtxNotSampled := trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: [16]byte{2},
|
||||
SpanID: [8]byte{2},
|
||||
Remote: true,
|
||||
TraceFlags: 0,
|
||||
})
|
||||
|
||||
// define test cases
|
||||
testCases := []struct {
|
||||
Name string
|
||||
TraceHeaderConfig otelchi.TraceHeaderConfig
|
||||
SpanContext trace.SpanContext
|
||||
ExpTraceResponseIDKey string
|
||||
ExpTraceResponseSampledKey string
|
||||
ExpTraceResponseSampledKeyVal bool
|
||||
}{
|
||||
{
|
||||
Name: "Default Trace Config, Trace Sampled",
|
||||
TraceHeaderConfig: otelchi.TraceHeaderConfig{},
|
||||
SpanContext: spanCtxSampled,
|
||||
ExpTraceResponseIDKey: otelchi.DefaultTraceIDResponseHeaderKey,
|
||||
ExpTraceResponseSampledKey: otelchi.DefaultTraceSampledResponseHeaderKey,
|
||||
ExpTraceResponseSampledKeyVal: true,
|
||||
},
|
||||
{
|
||||
Name: "Default Trace Config, Trace Not Sampled",
|
||||
TraceHeaderConfig: otelchi.TraceHeaderConfig{},
|
||||
SpanContext: spanCtxNotSampled,
|
||||
ExpTraceResponseIDKey: otelchi.DefaultTraceIDResponseHeaderKey,
|
||||
ExpTraceResponseSampledKey: otelchi.DefaultTraceSampledResponseHeaderKey,
|
||||
ExpTraceResponseSampledKeyVal: false,
|
||||
},
|
||||
{
|
||||
Name: "Custom Trace Config, Trace Sampled",
|
||||
TraceHeaderConfig: otelchi.TraceHeaderConfig{
|
||||
TraceIDHeader: "X-Custom-Trace-ID",
|
||||
TraceSampledHeader: "X-Custom-Trace-Sampled",
|
||||
},
|
||||
SpanContext: spanCtxSampled,
|
||||
ExpTraceResponseIDKey: "X-Custom-Trace-ID",
|
||||
ExpTraceResponseSampledKey: "X-Custom-Trace-Sampled",
|
||||
ExpTraceResponseSampledKeyVal: true,
|
||||
},
|
||||
{
|
||||
Name: "Custom Trace Config, Trace Not Sampled",
|
||||
TraceHeaderConfig: otelchi.TraceHeaderConfig{
|
||||
TraceIDHeader: "X-Custom-Trace-ID",
|
||||
TraceSampledHeader: "X-Custom-Trace-Sampled",
|
||||
},
|
||||
SpanContext: spanCtxNotSampled,
|
||||
ExpTraceResponseIDKey: "X-Custom-Trace-ID",
|
||||
ExpTraceResponseSampledKey: "X-Custom-Trace-Sampled",
|
||||
ExpTraceResponseSampledKeyVal: false,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
// configure router
|
||||
router := chi.NewRouter()
|
||||
router.Use(
|
||||
otelchi.Middleware(
|
||||
"foobar",
|
||||
otelchi.WithChiRoutes(router),
|
||||
otelchi.WithTraceResponseHeaders(testCase.TraceHeaderConfig),
|
||||
),
|
||||
)
|
||||
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
||||
router.HandleFunc("/book/{title}", ok)
|
||||
|
||||
// execute requests
|
||||
r0 := httptest.NewRequest("GET", "/user/123", nil)
|
||||
r0 = r0.WithContext(trace.ContextWithRemoteSpanContext(context.Background(), testCase.SpanContext))
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, r0)
|
||||
|
||||
// check response headers
|
||||
require.Equal(t, testCase.SpanContext.TraceID().String(), w.Header().Get(testCase.ExpTraceResponseIDKey))
|
||||
require.Equal(t, fmt.Sprintf("%v", testCase.ExpTraceResponseSampledKeyVal), w.Header().Get(testCase.ExpTraceResponseSampledKey))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithPublicEndpoint(t *testing.T) {
|
||||
// prepare router and span recorder
|
||||
router, spanRecorder := newSDKTestRouter("foobar", true, otelchi.WithPublicEndpoint())
|
||||
|
||||
// prepare remote span context
|
||||
remoteSpanCtx := trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: trace.TraceID{0x01},
|
||||
SpanID: trace.SpanID{0x01},
|
||||
Remote: true,
|
||||
})
|
||||
|
||||
// prepare http request & inject remote span context into it
|
||||
endpointURL := "/with/public/endpoint"
|
||||
req := httptest.NewRequest(http.MethodGet, endpointURL, nil)
|
||||
ctx := trace.ContextWithSpanContext(context.Background(), remoteSpanCtx)
|
||||
(propagation.TraceContext{}).Inject(ctx, propagation.HeaderCarrier(req.Header))
|
||||
|
||||
// configure router handler
|
||||
router.HandleFunc(endpointURL, func(w http.ResponseWriter, r *http.Request) {
|
||||
// get span from request context
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
spanCtx := span.SpanContext()
|
||||
|
||||
// ensure it is not equal to the remote span context
|
||||
require.False(t, spanCtx.Equal(remoteSpanCtx))
|
||||
require.True(t, spanCtx.IsValid())
|
||||
require.False(t, spanCtx.IsRemote())
|
||||
})
|
||||
|
||||
// execute http request
|
||||
executeRequests(router, []*http.Request{req})
|
||||
|
||||
// get recorded spans
|
||||
recordedSpans := spanRecorder.Ended()
|
||||
require.Len(t, recordedSpans, 1)
|
||||
|
||||
links := recordedSpans[0].Links()
|
||||
require.Len(t, links, 1)
|
||||
require.True(t, remoteSpanCtx.Equal(links[0].SpanContext))
|
||||
}
|
||||
|
||||
func TestWithPublicEndpointFn(t *testing.T) {
|
||||
// prepare remote span context
|
||||
remoteSpanCtx := trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: trace.TraceID{0x01},
|
||||
SpanID: trace.SpanID{0x01},
|
||||
Remote: true,
|
||||
})
|
||||
|
||||
// prepare test cases
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Fn func(r *http.Request) bool
|
||||
HandlerAssert func(t *testing.T, spanCtx trace.SpanContext)
|
||||
SpansAssert func(t *testing.T, spanCtx trace.SpanContext, spans []sdktrace.ReadOnlySpan)
|
||||
}{
|
||||
{
|
||||
Name: "Function Always Return True",
|
||||
Fn: func(r *http.Request) bool { return true },
|
||||
HandlerAssert: func(t *testing.T, spanCtx trace.SpanContext) {
|
||||
// ensure it is not equal to the remote span context
|
||||
require.False(t, spanCtx.Equal(remoteSpanCtx))
|
||||
require.True(t, spanCtx.IsValid())
|
||||
|
||||
// ensure it is not remote span
|
||||
require.False(t, spanCtx.IsRemote())
|
||||
},
|
||||
SpansAssert: func(t *testing.T, spanCtx trace.SpanContext, spans []sdktrace.ReadOnlySpan) {
|
||||
// ensure spans length
|
||||
require.Len(t, spans, 1)
|
||||
|
||||
// ensure the span has been linked
|
||||
links := spans[0].Links()
|
||||
require.Len(t, links, 1)
|
||||
require.True(t, remoteSpanCtx.Equal(links[0].SpanContext))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Function Always Return False",
|
||||
Fn: func(r *http.Request) bool { return false },
|
||||
HandlerAssert: func(t *testing.T, spanCtx trace.SpanContext) {
|
||||
// ensure the span is child of the remote span
|
||||
require.Equal(t, remoteSpanCtx.TraceID(), spanCtx.TraceID())
|
||||
require.True(t, spanCtx.IsValid())
|
||||
|
||||
// ensure it is not remote span
|
||||
require.False(t, spanCtx.IsRemote())
|
||||
},
|
||||
SpansAssert: func(t *testing.T, spanCtx trace.SpanContext, spans []sdktrace.ReadOnlySpan) {
|
||||
// ensure spans length
|
||||
require.Len(t, spans, 1, "unexpected span length")
|
||||
|
||||
// ensure the span has no links
|
||||
links := spans[0].Links()
|
||||
require.Len(t, links, 0)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// execute test cases
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
|
||||
// prepare router and span recorder
|
||||
router, spanRecorder := newSDKTestRouter(
|
||||
"foobar",
|
||||
true,
|
||||
otelchi.WithPublicEndpointFn(testCase.Fn),
|
||||
)
|
||||
|
||||
// prepare http request & inject remote span context into it
|
||||
endpointURL := "/with/public/endpoint"
|
||||
req := httptest.NewRequest(http.MethodGet, endpointURL, nil)
|
||||
ctx := trace.ContextWithSpanContext(context.Background(), remoteSpanCtx)
|
||||
(propagation.TraceContext{}).Inject(ctx, propagation.HeaderCarrier(req.Header))
|
||||
|
||||
// configure router handler
|
||||
router.HandleFunc(endpointURL, func(w http.ResponseWriter, r *http.Request) {
|
||||
// assert handler
|
||||
span := trace.SpanFromContext(r.Context())
|
||||
testCase.HandlerAssert(t, span.SpanContext())
|
||||
})
|
||||
|
||||
// execute http request
|
||||
executeRequests(router, []*http.Request{req})
|
||||
|
||||
// assert recorded spans
|
||||
testCase.SpansAssert(t, remoteSpanCtx, spanRecorder.Ended())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertSpan(t *testing.T, span sdktrace.ReadOnlySpan, name string, kind trace.SpanKind, attrs ...attribute.KeyValue) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, name, span.Name())
|
||||
assert.Equal(t, kind, span.SpanKind())
|
||||
|
||||
got := make(map[attribute.Key]attribute.Value, len(span.Attributes()))
|
||||
for _, a := range span.Attributes() {
|
||||
got[a.Key] = a.Value
|
||||
}
|
||||
for _, want := range attrs {
|
||||
if !assert.Contains(t, got, want.Key) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, want.Value, got[want.Key])
|
||||
}
|
||||
}
|
||||
|
||||
func ok(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func newSDKTestRouter(serverName string, withChiRoutes bool, opts ...otelchi.Option) (*chi.Mux, *tracetest.SpanRecorder) {
|
||||
spanRecorder := tracetest.NewSpanRecorder()
|
||||
tracerProvider := sdktrace.NewTracerProvider(
|
||||
// set the tracer provider to always sample trace, this is important
|
||||
// because if we don't set this, sometimes there are traces that
|
||||
// won't be sampled (recorded), so we need to set this option
|
||||
// to ensure every trace in this test is recorded.
|
||||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||
)
|
||||
tracerProvider.RegisterSpanProcessor(spanRecorder)
|
||||
|
||||
opts = append(opts, otelchi.WithTracerProvider(tracerProvider))
|
||||
|
||||
router := chi.NewRouter()
|
||||
if withChiRoutes {
|
||||
opts = append(opts, otelchi.WithChiRoutes(router))
|
||||
}
|
||||
router.Use(otelchi.Middleware(serverName, opts...))
|
||||
|
||||
return router, spanRecorder
|
||||
}
|
||||
|
||||
type spanValueCheck struct {
|
||||
Name string
|
||||
Kind trace.SpanKind
|
||||
Attributes []attribute.KeyValue
|
||||
}
|
||||
|
||||
func getSemanticAttributes(serverName string, httpStatusCode int, httpMethod, httpRoute string) []attribute.KeyValue {
|
||||
return []attribute.KeyValue{
|
||||
attribute.String("net.host.name", serverName),
|
||||
attribute.Int("http.status_code", httpStatusCode),
|
||||
attribute.String("http.method", httpMethod),
|
||||
attribute.String("http.route", httpRoute),
|
||||
}
|
||||
}
|
||||
|
||||
func checkSpans(t *testing.T, spans []sdktrace.ReadOnlySpan, valueChecks []spanValueCheck) {
|
||||
t.Helper()
|
||||
|
||||
for i := 0; i < len(spans); i++ {
|
||||
span := spans[i]
|
||||
valueCheck := valueChecks[i]
|
||||
assertSpan(t, span, valueCheck.Name, valueCheck.Kind, valueCheck.Attributes...)
|
||||
}
|
||||
}
|
||||
|
||||
func executeRequests(router *chi.Mux, reqs []*http.Request) {
|
||||
w := httptest.NewRecorder()
|
||||
for _, r := range reqs {
|
||||
router.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
20
test/cases/version_test.go
Normal file
20
test/cases/version_test.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package otelchi_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"toastielab.dev/toastie-stuff/otelchi"
|
||||
)
|
||||
|
||||
// regex taken from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||
var versionRegex = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)` +
|
||||
`(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)` +
|
||||
`(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
|
||||
`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
|
||||
|
||||
func TestVersionSemver(t *testing.T) {
|
||||
v := otelchi.Version()
|
||||
assert.NotNil(t, versionRegex.FindStringSubmatch(v), "version is not semver: %s", v)
|
||||
}
|
34
test/infras/Dockerfile
Normal file
34
test/infras/Dockerfile
Normal file
|
@ -0,0 +1,34 @@
|
|||
FROM debian:10.13
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
curl \
|
||||
git \
|
||||
unzip \
|
||||
wget \
|
||||
make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Gobrew
|
||||
ENV PATH="/root/.gobrew/current/bin:/root/.gobrew/bin:$PATH"
|
||||
RUN curl -sL https://raw.githubusercontent.com/kevincobain2000/gobrew/v1.10.11/git.io.sh | bash
|
||||
|
||||
# Set Working Directory
|
||||
WORKDIR /go/src/toastielab.dev/toastie-stuff/otelchi
|
||||
|
||||
# Install Multiple Go Versions
|
||||
ARG GO_VERSIONS
|
||||
COPY ./test/infras/install_go.sh ./test/infras/
|
||||
RUN chmod +x ./test/infras/install_go.sh && sh ./test/infras/install_go.sh
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download -x
|
||||
|
||||
COPY ./test/infras/entrypoint.sh ./test/infras/
|
||||
RUN chmod +x ./test/infras/entrypoint.sh
|
||||
|
||||
ENV GO_VERSIONS=$GO_VERSIONS
|
||||
|
||||
COPY . .
|
||||
|
||||
ENTRYPOINT [ "./test/infras/entrypoint.sh" ]
|
18
test/infras/entrypoint.sh
Normal file
18
test/infras/entrypoint.sh
Normal file
|
@ -0,0 +1,18 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Terminate script immediately on any errors
|
||||
set -e
|
||||
|
||||
# Define Go versions
|
||||
go_versions=${GO_VERSIONS}
|
||||
|
||||
echo ${GO_VERSIONS}
|
||||
|
||||
# Iterate over each version
|
||||
for version in $go_versions; do
|
||||
# Set Go version using gobrew
|
||||
gobrew use "$version"
|
||||
|
||||
# Execute Test as defined in the Makefile
|
||||
make go-test
|
||||
done
|
9
test/infras/install_go.sh
Normal file
9
test/infras/install_go.sh
Normal file
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Define Go versions
|
||||
go_versions=${GO_VERSIONS}
|
||||
|
||||
# Install each Go version using gobrew
|
||||
for version in $go_versions; do
|
||||
gobrew install "$version"
|
||||
done
|
6
version.go
Normal file
6
version.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package otelchi
|
||||
|
||||
// Version is the current release version of otelchi in use.
|
||||
func Version() string {
|
||||
return "0.10.0"
|
||||
}
|
Loading…
Add table
Reference in a new issue