Skip to content

Commit f5fe145

Browse files
committed
feat: add otel support
1 parent 01f82ba commit f5fe145

File tree

8 files changed

+218
-8
lines changed

8 files changed

+218
-8
lines changed

api/globalconfig/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/evcc-io/evcc/util"
1616
"github.com/evcc-io/evcc/util/config"
1717
"github.com/evcc-io/evcc/util/modbus"
18+
"github.com/evcc-io/evcc/util/otel"
1819
)
1920

2021
type All struct {
@@ -45,6 +46,7 @@ type All struct {
4546
Site map[string]any
4647
Loadpoints []config.Named
4748
Circuits []config.Named
49+
Otel otel.Config
4850
}
4951

5052
type Javascript struct {

cmd/root.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"net/http"
@@ -12,6 +13,7 @@ import (
1213
"syscall"
1314
"time"
1415

16+
"github.com/evcc-io/evcc/cmd/shutdown"
1517
"github.com/evcc-io/evcc/core"
1618
"github.com/evcc-io/evcc/core/keys"
1719
"github.com/evcc-io/evcc/push"
@@ -22,6 +24,7 @@ import (
2224
"github.com/evcc-io/evcc/server/updater"
2325
"github.com/evcc-io/evcc/util"
2426
"github.com/evcc-io/evcc/util/auth"
27+
"github.com/evcc-io/evcc/util/otel"
2528
"github.com/evcc-io/evcc/util/pipe"
2629
"github.com/evcc-io/evcc/util/sponsor"
2730
"github.com/evcc-io/evcc/util/telemetry"
@@ -164,6 +167,22 @@ func runRoot(cmd *cobra.Command, args []string) {
164167
network.Start(conf.Network)
165168
}
166169

170+
// setup OpenTelemetry
171+
ctx := context.Background()
172+
if err == nil && conf.Otel.Enabled {
173+
if initErr := otel.Init(ctx, conf.Otel); initErr != nil {
174+
log.WARN.Printf("OpenTelemetry initialization failed: %v", initErr)
175+
// Don't fail startup if otel fails
176+
} else {
177+
// Register shutdown handler
178+
shutdown.Register(func() {
179+
if shutdownErr := otel.Shutdown(context.Background()); shutdownErr != nil {
180+
log.WARN.Printf("OpenTelemetry shutdown failed: %v", shutdownErr)
181+
}
182+
})
183+
}
184+
}
185+
167186
// start broadcasting values
168187
tee := new(util.Tee)
169188
valueChan := make(chan util.Param, 64)

evcc.dist.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ levels:
4040
cache: error
4141
db: error
4242

43+
# otel:
44+
# enabled: true # enable OpenTelemetry
45+
# endpoint: "localhost:4317" # OTLP endpoint (gRPC)
46+
# protocol: "grpc" # "grpc" or "http"
47+
# insecure: false # Set to true for insecure connections
48+
4349
# modbus proxy for allowing external programs to reuse the evcc modbus connection
4450
# each entry will start a proxy instance at the given port speaking Modbus TCP and
4551
# relaying to the given modbus downstream device (either TCP or RTU, RS485 or TCP)

go.mod

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ require (
103103
github.com/volkszaehler/mbmd v0.0.0-20250808161051-499ae856f44e
104104
github.com/writeas/go-strip-markdown/v2 v2.1.1
105105
gitlab.com/bboehmke/sunny v0.16.0
106+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
107+
go.opentelemetry.io/otel v1.38.0
108+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
109+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0
110+
go.opentelemetry.io/otel/sdk v1.38.0
106111
go.uber.org/mock v0.6.0
107112
go.yaml.in/yaml/v4 v4.0.0-rc.3
108113
golang.org/x/crypto v0.45.0
@@ -140,7 +145,7 @@ require (
140145
github.com/beorn7/perks v1.0.1 // indirect
141146
github.com/bitly/go-simplejson v0.5.0 // indirect
142147
github.com/breml/rootcerts v0.2.21 // indirect
143-
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
148+
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
144149
github.com/cespare/xxhash/v2 v2.3.0 // indirect
145150
github.com/clipperhouse/displaywidth v0.6.0 // indirect
146151
github.com/clipperhouse/stringish v0.1.1 // indirect
@@ -164,6 +169,8 @@ require (
164169
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
165170
github.com/go-kit/log v0.2.1 // indirect
166171
github.com/go-logfmt/logfmt v0.6.0 // indirect
172+
github.com/go-logr/logr v1.4.3 // indirect
173+
github.com/go-logr/stdr v1.2.2 // indirect
167174
github.com/go-openapi/jsonpointer v0.22.0 // indirect
168175
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
169176
github.com/go-playground/locales v0.14.1 // indirect
@@ -178,6 +185,7 @@ require (
178185
github.com/gorilla/websocket v1.5.3 // indirect
179186
github.com/gosimple/unidecode v1.0.1 // indirect
180187
github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect
188+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
181189
github.com/huandu/xstrings v1.5.0 // indirect
182190
github.com/inconshreveable/mousetrap v1.1.0 // indirect
183191
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
@@ -236,12 +244,18 @@ require (
236244
github.com/woodsbury/decimal128 v1.4.0 // indirect
237245
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
238246
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
247+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
248+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
249+
go.opentelemetry.io/otel/metric v1.38.0 // indirect
250+
go.opentelemetry.io/otel/trace v1.38.0 // indirect
251+
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
239252
go.yaml.in/yaml/v2 v2.4.3 // indirect
240253
go.yaml.in/yaml/v3 v3.0.4 // indirect
241254
golang.org/x/mod v0.30.0 // indirect
242255
golang.org/x/sys v0.38.0 // indirect
243256
golang.org/x/term v0.37.0 // indirect
244-
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
257+
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
258+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
245259
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
246260
gopkg.in/sourcemap.v1 v1.0.5 // indirect
247261
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n
114114
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
115115
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
116116
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
117-
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
118-
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
117+
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
118+
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
119119
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
120120
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
121121
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
245245
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
246246
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
247247
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
248+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
248249
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
249250
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
250251
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
365366
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
366367
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
367368
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
369+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
370+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
368371
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
369372
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
370373
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
665668
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
666669
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
667670
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
668-
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
669-
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
671+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
672+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
670673
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
671674
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
672675
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=
786789
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
787790
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
788791
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
792+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
793+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
789794
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
790795
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
796+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
797+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
798+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
799+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
800+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
801+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
791802
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
792803
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
793804
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
796807
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
797808
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
798809
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
810+
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
811+
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
799812
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
800813
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
801814
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
10051018
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
10061019
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
10071020
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
1008-
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
1009-
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
1021+
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
1022+
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
1023+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
1024+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
10101025
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
10111026
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
10121027
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=

server/http.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/evcc-io/evcc/util"
2020
"github.com/evcc-io/evcc/util/auth"
2121
"github.com/evcc-io/evcc/util/config"
22+
"github.com/evcc-io/evcc/util/otel"
2223
"github.com/evcc-io/evcc/util/telemetry"
2324
"github.com/go-http-utils/etag"
2425
"github.com/gorilla/handlers"
@@ -226,6 +227,9 @@ func (s *HTTPd) RegisterSystemHandler(site *core.Site, valueChan chan<- util.Par
226227

227228
// api
228229
api := router.PathPrefix("/api").Subrouter()
230+
// Only attach tracing to API
231+
api.Use(otel.HTTPMiddleware)
232+
229233
api.Use(jsonHandler)
230234
api.Use(handlers.CompressHandler)
231235
api.Use(handlers.CORS(

util/otel/http.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package otel
2+
3+
import (
4+
"net/http"
5+
6+
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
7+
"go.opentelemetry.io/otel/propagation"
8+
)
9+
10+
// HTTPMiddleware returns an HTTP middleware that instruments HTTP requests
11+
func HTTPMiddleware(next http.Handler) http.Handler {
12+
return otelhttp.NewHandler(next, "",
13+
otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
14+
return r.Method + " " + r.URL.Path
15+
}),
16+
)
17+
}
18+
19+
// HTTPClient returns an HTTP client with OpenTelemetry instrumentation
20+
func HTTPClient(base http.RoundTripper) http.RoundTripper {
21+
if base == nil {
22+
base = http.DefaultTransport
23+
}
24+
25+
return otelhttp.NewTransport(
26+
base,
27+
otelhttp.WithPropagators(propagation.NewCompositeTextMapPropagator(
28+
propagation.TraceContext{},
29+
propagation.Baggage{},
30+
)),
31+
)
32+
}

util/otel/otel.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package otel
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
9+
10+
"github.com/evcc-io/evcc/util"
11+
"go.opentelemetry.io/otel"
12+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
13+
"go.opentelemetry.io/otel/propagation"
14+
"go.opentelemetry.io/otel/sdk/resource"
15+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
16+
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
17+
)
18+
19+
var (
20+
log = util.NewLogger("otel")
21+
)
22+
23+
// Config holds OpenTelemetry configuration
24+
type Config struct {
25+
Enabled bool `json:"enabled"`
26+
Endpoint string `json:"endpoint"`
27+
Protocol string `json:"protocol"` // "grpc" or "http"
28+
Insecure bool `json:"insecure"`
29+
}
30+
31+
// Init initializes OpenTelemetry tracing
32+
func Init(ctx context.Context, cfg Config) error {
33+
if !cfg.Enabled {
34+
return nil
35+
}
36+
37+
if cfg.Endpoint == "" {
38+
return fmt.Errorf("otel endpoint is required when enabled")
39+
}
40+
41+
var exporter sdktrace.SpanExporter
42+
var err error
43+
44+
// Default to grpc if protocol not specified
45+
protocol := cfg.Protocol
46+
switch protocol {
47+
case "http":
48+
opts := []otlptracehttp.Option{
49+
otlptracehttp.WithEndpoint(cfg.Endpoint),
50+
}
51+
if cfg.Insecure {
52+
opts = append(opts, otlptracehttp.WithInsecure())
53+
}
54+
exporter, err = otlptracehttp.New(ctx, opts...)
55+
default:
56+
protocol = "grpc"
57+
opts := []otlptracegrpc.Option{
58+
otlptracegrpc.WithEndpoint(cfg.Endpoint),
59+
}
60+
if cfg.Insecure {
61+
opts = append(opts, otlptracegrpc.WithInsecure())
62+
}
63+
exporter, err = otlptracegrpc.New(ctx, opts...)
64+
}
65+
66+
if err != nil {
67+
return fmt.Errorf("failed to create otel exporter: %w", err)
68+
}
69+
70+
res, err := resource.New(ctx,
71+
resource.WithAttributes(
72+
semconv.ServiceName("evcc"),
73+
semconv.ServiceVersion(util.FormattedVersion()),
74+
),
75+
)
76+
if err != nil {
77+
return fmt.Errorf("failed to create otel resource: %w", err)
78+
}
79+
80+
tp := sdktrace.NewTracerProvider(
81+
sdktrace.WithBatcher(exporter),
82+
sdktrace.WithResource(res),
83+
sdktrace.WithSampler(sdktrace.AlwaysSample()),
84+
)
85+
86+
otel.SetTracerProvider(tp)
87+
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
88+
propagation.TraceContext{},
89+
propagation.Baggage{},
90+
))
91+
92+
log.INFO.Printf("OpenTelemetry tracing enabled: endpoint=%s, protocol=%s", cfg.Endpoint, protocol)
93+
return nil
94+
}
95+
96+
// Shutdown gracefully shuts down the tracer provider
97+
func Shutdown(ctx context.Context) error {
98+
tp := otel.GetTracerProvider()
99+
if tp == nil {
100+
return nil
101+
}
102+
103+
// Check if it's an SDK tracer provider that supports Shutdown
104+
sdkTracerProvider, ok := tp.(*sdktrace.TracerProvider)
105+
if !ok {
106+
return nil
107+
}
108+
109+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
110+
defer cancel()
111+
112+
if err := sdkTracerProvider.Shutdown(ctx); err != nil {
113+
return fmt.Errorf("failed to shutdown otel tracer provider: %w", err)
114+
}
115+
116+
log.INFO.Println("OpenTelemetry tracing shutdown complete")
117+
return nil
118+
}

0 commit comments

Comments
 (0)