diff --git a/api/globalconfig/types.go b/api/globalconfig/types.go index e45ea26b3e..0b19f6946a 100644 --- a/api/globalconfig/types.go +++ b/api/globalconfig/types.go @@ -15,6 +15,7 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/config" "github.com/evcc-io/evcc/util/modbus" + "github.com/evcc-io/evcc/util/otel" ) type All struct { @@ -45,6 +46,7 @@ type All struct { Site map[string]any Loadpoints []config.Named Circuits []config.Named + Otel otel.Config } type Javascript struct { diff --git a/cmd/root.go b/cmd/root.go index 16d8037787..1462a78610 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "errors" "fmt" "net/http" @@ -12,6 +13,7 @@ import ( "syscall" "time" + "github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/core" "github.com/evcc-io/evcc/core/keys" "github.com/evcc-io/evcc/push" @@ -22,6 +24,7 @@ import ( "github.com/evcc-io/evcc/server/updater" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/auth" + "github.com/evcc-io/evcc/util/otel" "github.com/evcc-io/evcc/util/pipe" "github.com/evcc-io/evcc/util/sponsor" "github.com/evcc-io/evcc/util/telemetry" @@ -164,6 +167,22 @@ func runRoot(cmd *cobra.Command, args []string) { network.Start(conf.Network) } + // setup OpenTelemetry + ctx := context.Background() + if err == nil && conf.Otel.Enabled { + if initErr := otel.Init(ctx, conf.Otel); initErr != nil { + log.WARN.Printf("OpenTelemetry initialization failed: %v", initErr) + // Don't fail startup if otel fails + } else { + // Register shutdown handler + shutdown.Register(func() { + if shutdownErr := otel.Shutdown(context.Background()); shutdownErr != nil { + log.WARN.Printf("OpenTelemetry shutdown failed: %v", shutdownErr) + } + }) + } + } + // start broadcasting values tee := new(util.Tee) valueChan := make(chan util.Param, 64) diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 0d6f2efc31..60516eff4e 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -40,6 +40,12 @@ levels: cache: error db: error +# otel: +# enabled: true # enable OpenTelemetry +# endpoint: "localhost:4317" # OTLP endpoint (gRPC) +# protocol: "grpc" # "grpc" or "http" +# insecure: false # Set to true for insecure connections + # modbus proxy for allowing external programs to reuse the evcc modbus connection # each entry will start a proxy instance at the given port speaking Modbus TCP and # relaying to the given modbus downstream device (either TCP or RTU, RS485 or TCP) diff --git a/go.mod b/go.mod index 65cdf55fcd..f97aeb27fe 100644 --- a/go.mod +++ b/go.mod @@ -103,6 +103,11 @@ require ( github.com/volkszaehler/mbmd v0.0.0-20250808161051-499ae856f44e github.com/writeas/go-strip-markdown/v2 v2.1.1 gitlab.com/bboehmke/sunny v0.16.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 go.uber.org/mock v0.6.0 go.yaml.in/yaml/v4 v4.0.0-rc.3 golang.org/x/crypto v0.45.0 @@ -140,7 +145,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-simplejson v0.5.0 // indirect github.com/breml/rootcerts v0.2.21 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/displaywidth v0.6.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect @@ -164,6 +169,8 @@ require ( github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.0 // indirect github.com/go-openapi/swag/jsonname v0.24.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -178,6 +185,7 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect @@ -236,12 +244,18 @@ require ( github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect gopkg.in/go-playground/validator.v9 v9.31.0 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e130879f54..ce92751379 100644 --- a/go.sum +++ b/go.sum @@ -114,8 +114,8 @@ github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 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/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -245,6 +245,7 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +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= @@ -365,6 +366,8 @@ github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa/go.mod h1:kdOd86/VGF github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -665,8 +668,8 @@ github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dG github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -786,8 +789,16 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 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/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= @@ -796,6 +807,8 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= @@ -1005,8 +1018,10 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= diff --git a/server/http.go b/server/http.go index 82375b05dd..453d23a299 100644 --- a/server/http.go +++ b/server/http.go @@ -19,6 +19,7 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/auth" "github.com/evcc-io/evcc/util/config" + "github.com/evcc-io/evcc/util/otel" "github.com/evcc-io/evcc/util/telemetry" "github.com/go-http-utils/etag" "github.com/gorilla/handlers" @@ -226,6 +227,9 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, valueChan chan<- util.Par // api api := router.PathPrefix("/api").Subrouter() + // Only attach tracing to API + api.Use(otel.HTTPMiddleware) + api.Use(jsonHandler) api.Use(handlers.CompressHandler) api.Use(handlers.CORS( diff --git a/util/otel/http.go b/util/otel/http.go new file mode 100644 index 0000000000..df562aba4a --- /dev/null +++ b/util/otel/http.go @@ -0,0 +1,32 @@ +package otel + +import ( + "net/http" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/propagation" +) + +// HTTPMiddleware returns an HTTP middleware that instruments HTTP requests +func HTTPMiddleware(next http.Handler) http.Handler { + return otelhttp.NewHandler(next, "", + otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { + return r.Method + " " + r.URL.Path + }), + ) +} + +// HTTPClient returns an HTTP client with OpenTelemetry instrumentation +func HTTPClient(base http.RoundTripper) http.RoundTripper { + if base == nil { + base = http.DefaultTransport + } + + return otelhttp.NewTransport( + base, + otelhttp.WithPropagators(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )), + ) +} diff --git a/util/otel/otel.go b/util/otel/otel.go new file mode 100644 index 0000000000..eea1b6f071 --- /dev/null +++ b/util/otel/otel.go @@ -0,0 +1,117 @@ +package otel + +import ( + "context" + "fmt" + "time" + + "github.com/evcc-io/evcc/util" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "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.24.0" +) + +var ( + log = util.NewLogger("otel") +) + +// Config holds OpenTelemetry configuration +type Config struct { + Enabled bool `json:"enabled"` + Endpoint string `json:"endpoint"` + Protocol string `json:"protocol"` // "grpc" or "http" + Insecure bool `json:"insecure"` +} + +// Init initializes OpenTelemetry tracing +func Init(ctx context.Context, cfg Config) error { + if !cfg.Enabled { + return nil + } + + if cfg.Endpoint == "" { + return fmt.Errorf("otel endpoint is required when enabled") + } + + var exporter sdktrace.SpanExporter + var err error + + // Default to grpc if protocol not specified + protocol := cfg.Protocol + switch protocol { + case "http": + opts := []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(cfg.Endpoint), + } + if cfg.Insecure { + opts = append(opts, otlptracehttp.WithInsecure()) + } + exporter, err = otlptracehttp.New(ctx, opts...) + default: + protocol = "grpc" + opts := []otlptracegrpc.Option{ + otlptracegrpc.WithEndpoint(cfg.Endpoint), + } + if cfg.Insecure { + opts = append(opts, otlptracegrpc.WithInsecure()) + } + exporter, err = otlptracegrpc.New(ctx, opts...) + } + + if err != nil { + return fmt.Errorf("failed to create otel exporter: %w", err) + } + + res, err := resource.New(ctx, + resource.WithAttributes( + semconv.ServiceName("evcc"), + semconv.ServiceVersion(util.FormattedVersion()), + ), + ) + if err != nil { + return fmt.Errorf("failed to create otel resource: %w", err) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(res), + sdktrace.WithSampler(sdktrace.AlwaysSample()), + ) + + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + log.INFO.Printf("OpenTelemetry tracing enabled: endpoint=%s, protocol=%s", cfg.Endpoint, protocol) + return nil +} + +// Shutdown gracefully shuts down the tracer provider +func Shutdown(ctx context.Context) error { + tp := otel.GetTracerProvider() + if tp == nil { + return nil + } + + // Check if it's an SDK tracer provider that supports Shutdown + sdkTracerProvider, ok := tp.(*sdktrace.TracerProvider) + if !ok { + return nil + } + + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + if err := sdkTracerProvider.Shutdown(ctx); err != nil { + return fmt.Errorf("failed to shutdown otel tracer provider: %w", err) + } + + log.INFO.Println("OpenTelemetry tracing shutdown complete") + return nil +}