diff --git a/cmd/showbridge/main.go b/cmd/showbridge/main.go index 2c726ed..68ac932 100644 --- a/cmd/showbridge/main.go +++ b/cmd/showbridge/main.go @@ -13,6 +13,12 @@ import ( "github.com/jwetzell/showbridge-go" "github.com/jwetzell/showbridge-go/internal/config" "github.com/urfave/cli/v3" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + "go.opentelemetry.io/otel/trace" "sigs.k8s.io/yaml" ) @@ -55,6 +61,11 @@ func main() { return nil }, }, + &cli.BoolFlag{ + Name: "trace", + Value: false, + Usage: "enable OpenTelemetry tracing", + }, }, Action: run, } @@ -137,7 +148,23 @@ func run(ctx context.Context, c *cli.Command) error { commandLogger := slog.Default().With("component", "cmd") - router, moduleErrors, routeErrors := showbridge.NewRouter(config) + var tracer trace.Tracer + if c.Bool("trace") { + exporter, err := otlptracehttp.New(ctx) + if err != nil { + return fmt.Errorf("failed to create trace exporter: %w", err) + } + + tracerProvider := newTracerProvider(exporter) + otel.SetTracerProvider(tracerProvider) + defer tracerProvider.Shutdown(ctx) + + tracer = tracerProvider.Tracer("showbridge") + } else { + tracer = otel.Tracer("showbridge") + } + + router, moduleErrors, routeErrors := showbridge.NewRouter(config, tracer) for _, moduleError := range moduleErrors { commandLogger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error) @@ -160,3 +187,23 @@ func run(ctx context.Context, c *cli.Command) error { routerRunner.Wait() return nil } + +func newTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider { + r, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("showbridge"), + semconv.ServiceVersion(version), + ), + ) + + if err != nil { + panic(err) + } + + return sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp), + sdktrace.WithResource(r), + ) +} diff --git a/go.mod b/go.mod index d4855ae..0019965 100644 --- a/go.mod +++ b/go.mod @@ -16,22 +16,31 @@ require ( github.com/urfave/cli/v3 v3.6.2 gitlab.com/gomidi/midi/v2 v2.3.18 go.bug.st/serial v1.6.4 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 modernc.org/quickjs v0.17.1 sigs.k8s.io/yaml v1.6.0 ) require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/creack/goselect v0.1.2 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 // indirect github.com/go-audio/riff v1.0.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect github.com/icholy/digest v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -50,13 +59,20 @@ require ( github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/zaf/g711 v1.4.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.44.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect modernc.org/libc v1.67.1 // indirect diff --git a/go.sum b/go.sum index d5bc73b..a2241b2 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -20,6 +24,11 @@ github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -34,6 +43,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 h1:CWyXh/jylQWp2dtiV33mY4iSSp6yf4lmn+c7/tN+ObI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0/go.mod h1:nCLIt0w3Ept2NwF8ThLmrppXsfT07oC8k0XNDxd8sVU= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw= @@ -82,8 +93,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= @@ -98,8 +109,26 @@ gitlab.com/gomidi/midi/v2 v2.3.18 h1:sj2fOhtvOe+zI8YJe8qTxLw5zv0ntULLUDwcFOaZQbI gitlab.com/gomidi/midi/v2 v2.3.18/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw= go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= @@ -115,10 +144,18 @@ golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/router.go b/router.go index 7b72541..2da50c6 100644 --- a/router.go +++ b/router.go @@ -9,6 +9,10 @@ import ( "github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/route" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" ) type Router struct { @@ -18,16 +22,17 @@ type Router struct { RouteInstances []route.Route moduleWait sync.WaitGroup logger *slog.Logger + tracer trace.Tracer } -func NewRouter(config config.Config) (*Router, []module.ModuleError, []route.RouteError) { +func NewRouter(config config.Config, tracer trace.Tracer) (*Router, []module.ModuleError, []route.RouteError) { router := Router{ ModuleInstances: []module.Module{}, RouteInstances: []route.Route{}, logger: slog.Default().With("component", "router"), + tracer: tracer, } - router.logger.Debug("creating") var moduleErrors []module.ModuleError @@ -141,6 +146,8 @@ func (r *Router) Stop() { } func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []route.RouteIOError) { + spanCtx, span := r.tracer.Start(ctx, "router.input", trace.WithAttributes(attribute.String("source.id", sourceId)), trace.WithNewRoot()) + defer span.End() var routeIOErrors []route.RouteIOError routeFound := false @@ -151,9 +158,12 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) routeWaitGroup.Go(func() { routeFound = true - routeContext := context.WithValue(ctx, route.SourceContextKey, sourceId) + routeContext := context.WithValue(spanCtx, route.SourceContextKey, sourceId) - payload, err := routeInstance.ProcessPayload(routeContext, payload) + routeSpanCtx, routeSpan := r.tracer.Start(routeContext, "route.input", trace.WithAttributes(attribute.Int("route.index", routeIndex))) + defer routeSpan.End() + routeProcessCtx, routeSpan := r.tracer.Start(routeSpanCtx, "route.process") + payload, err := routeInstance.ProcessPayload(routeProcessCtx, payload) if err != nil { if routeIOErrors == nil { routeIOErrors = []route.RouteIOError{} @@ -163,7 +173,13 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) Index: routeIndex, ProcessError: err, }) + routeSpan.SetStatus(codes.Error, err.Error()) + routeSpan.RecordError(err) + routeSpan.End() return + } else { + routeSpan.SetStatus(codes.Ok, "route processing successful") + routeSpan.End() } if payload == nil { @@ -171,7 +187,8 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) return } - outputErrors := r.HandleOutput(routeContext, routeInstance.Output(), payload) + routeOutputCtx, routeOutputSpan := r.tracer.Start(routeSpanCtx, "route.output", trace.WithAttributes(attribute.String("destination.id", routeInstance.Output()))) + outputErrors := r.HandleOutput(routeOutputCtx, routeInstance.Output(), payload) if outputErrors != nil { if routeIOErrors == nil { routeIOErrors = []route.RouteIOError{} @@ -180,9 +197,15 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) Index: routeIndex, OutputErrors: outputErrors, }) + routeOutputSpan.SetStatus(codes.Error, "route output error") + for _, outputError := range outputErrors { + routeOutputSpan.RecordError(outputError) + } + } else { + routeOutputSpan.SetStatus(codes.Ok, "route output successful") } + routeOutputSpan.End() }) - } } routeWaitGroup.Wait() @@ -190,19 +213,35 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) } func (r *Router) HandleOutput(ctx context.Context, destinationId string, payload any) []error { - + spanCtx, span := r.tracer.Start(ctx, "router.output", trace.WithAttributes(attribute.String("destination.id", destinationId))) + defer span.End() var outputErrors []error for _, moduleInstance := range r.ModuleInstances { if moduleInstance.Id() == destinationId { - err := moduleInstance.Output(ctx, payload) + moduleSpanCtx, moduleSpan := r.tracer.Start(spanCtx, "module.output", trace.WithAttributes(attribute.String("module.id", moduleInstance.Id()), attribute.String("module.type", moduleInstance.Type()))) + err := moduleInstance.Output(moduleSpanCtx, payload) if err != nil { if outputErrors == nil { outputErrors = []error{} } outputErrors = append(outputErrors, err) - r.logger.Error("unable to route output", "module", moduleInstance.Id(), "error", err) + moduleSpan.SetStatus(codes.Error, err.Error()) + moduleSpan.RecordError(err) + r.logger.Error("module output encountered error", "module", moduleInstance.Id(), "error", err) + } else { + moduleSpan.SetStatus(codes.Ok, "module output successful") } + moduleSpan.End() } } + + if outputErrors != nil { + span.SetStatus(codes.Error, "router output error") + for _, outputError := range outputErrors { + span.RecordError(outputError) + } + } else { + span.SetStatus(codes.Ok, "router output successful") + } return outputErrors } diff --git a/router_test.go b/router_test.go index fd52b0f..988a3e2 100644 --- a/router_test.go +++ b/router_test.go @@ -12,6 +12,11 @@ import ( "github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/route" + "go.opentelemetry.io/otel" +) + +var ( + tracer = otel.Tracer("showbridge.test") ) type MockModule struct { @@ -61,7 +66,7 @@ func TestNewRouter(t *testing.T) { }, } - _, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig) + _, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig, tracer) if moduleErrors != nil { t.Fatalf("router should not have returned any module errors: %v", moduleErrors) @@ -99,7 +104,7 @@ func TestNewRouterUnknownModuleType(t *testing.T) { }, } - _, moduleErrors, _ := showbridge.NewRouter(routerConfig) + _, moduleErrors, _ := showbridge.NewRouter(routerConfig, tracer) if moduleErrors == nil { t.Fatalf("router should have returned 'unknown module' module errors") @@ -120,7 +125,7 @@ func TestNewRouterDuplicateModuleId(t *testing.T) { }, } - _, moduleErrors, _ := showbridge.NewRouter(routerConfig) + _, moduleErrors, _ := showbridge.NewRouter(routerConfig, tracer) if moduleErrors == nil { t.Fatalf("router should have returned 'duplicate id' module error") @@ -143,7 +148,7 @@ func TestRouterInputSingleRoute(t *testing.T) { }, } - router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig) + router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig, tracer) if moduleErrors != nil { t.Fatalf("router should not have returned any module errors: %v", moduleErrors) @@ -214,7 +219,7 @@ func TestRouterInputMultipleRoutes(t *testing.T) { }, } - router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig) + router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig, tracer) if moduleErrors != nil { t.Fatalf("router should not have returned any module errors: %v", moduleErrors) @@ -285,7 +290,7 @@ func TestRouterInputMultipleModules(t *testing.T) { }, } - router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig) + router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig, tracer) if moduleErrors != nil { t.Fatalf("router should not have returned any module errors: %v", moduleErrors)