Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee

feat: otel tracing

pdewey.com 5960d84a 9b52a8ea

verified
+337 -86
+1
CLAUDE.md
··· 450 450 | `ARABICA_FEED_INDEX_PATH` | ~/.local/share/arabica/feed-index.db | Firehose index BoltDB path | 451 451 | `ARABICA_MODERATORS_CONFIG` | - | Path to moderators JSON config | 452 452 | `ARABICA_PROFILE_CACHE_TTL` | 1h | Profile cache duration | 453 + | `OTEL_EXPORTER_OTLP_ENDPOINT` | localhost:4318 | OTLP HTTP endpoint for traces | 453 454 | `METRICS_PORT` | 9101 | Internal metrics server port (localhost only) | 454 455 | `SECURE_COOKIES` | false | Set true for HTTPS | 455 456 | `LOG_LEVEL` | info | debug/info/warn/error |
+14
cmd/server/main.go
··· 24 24 "arabica/internal/metrics" 25 25 "arabica/internal/moderation" 26 26 "arabica/internal/routing" 27 + "arabica/internal/tracing" 27 28 28 29 "github.com/prometheus/client_golang/prometheus/promhttp" 29 30 "github.com/rs/zerolog" ··· 64 65 } 65 66 66 67 log.Info().Msg("Starting Arabica Coffee Tracker") 68 + 69 + // Initialize OpenTelemetry tracing 70 + tp, err := tracing.Init(context.Background()) 71 + if err != nil { 72 + log.Warn().Err(err).Msg("Failed to initialize tracing, continuing without it") 73 + } else { 74 + defer func() { 75 + if err := tp.Shutdown(context.Background()); err != nil { 76 + log.Error().Err(err).Msg("Error shutting down tracer provider") 77 + } 78 + }() 79 + log.Info().Msg("OpenTelemetry tracing initialized") 80 + } 67 81 68 82 // Get port from env or use default 69 83 port := os.Getenv("PORT")
+57
dev/docker-compose.yaml
··· 1 + # Local dev support: Grafana + Tempo + Prometheus 2 + # Run with: docker compose -f dev/docker-compose.yaml up 3 + # 4 + # Services: 5 + # Grafana: http://localhost:3000 (admin/admin) 6 + # Tempo: http://localhost:3200 (OTLP ingest on :4318) 7 + # Prometheus: http://localhost:9090 (scrapes arabica metrics on :9101) 8 + # 9 + # Arabica will send traces to localhost:4318 by default (no config needed). 10 + 11 + services: 12 + tempo: 13 + image: grafana/tempo:2.7.2 14 + volumes: 15 + - ./tempo.yaml:/etc/tempo.yaml:ro 16 + - tempo-data:/var/tempo 17 + command: ["-config.file=/etc/tempo.yaml"] 18 + ports: 19 + - "4318:4318" # OTLP HTTP receiver 20 + - "3200:3200" # Tempo API 21 + extra_hosts: 22 + - "host.docker.internal:host-gateway" 23 + 24 + prometheus: 25 + image: prom/prometheus:v3.4.0 26 + volumes: 27 + - ./prometheus.yaml:/etc/prometheus/prometheus.yml:ro 28 + - prometheus-data:/prometheus 29 + command: 30 + - "--config.file=/etc/prometheus/prometheus.yml" 31 + - "--web.enable-remote-write-receiver" 32 + - "--storage.tsdb.path=/prometheus" 33 + ports: 34 + - "9090:9090" 35 + extra_hosts: 36 + - "host.docker.internal:host-gateway" 37 + 38 + grafana: 39 + image: grafana/grafana:11.6.1 40 + volumes: 41 + - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml:ro 42 + - ../grafana:/var/lib/grafana/dashboards:ro 43 + - grafana-data:/var/lib/grafana 44 + environment: 45 + GF_AUTH_ANONYMOUS_ENABLED: "true" 46 + GF_AUTH_ANONYMOUS_ORG_ROLE: Admin 47 + GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /var/lib/grafana/dashboards/arabica-prometheus.json 48 + ports: 49 + - "3000:3000" 50 + depends_on: 51 + - tempo 52 + - prometheus 53 + 54 + volumes: 55 + tempo-data: 56 + prometheus-data: 57 + grafana-data:
+20
dev/grafana-datasources.yaml
··· 1 + apiVersion: 1 2 + 3 + datasources: 4 + - name: Prometheus 5 + type: prometheus 6 + access: proxy 7 + url: http://prometheus:9090 8 + isDefault: true 9 + editable: true 10 + 11 + - name: Tempo 12 + type: tempo 13 + access: proxy 14 + url: http://tempo:3200 15 + editable: true 16 + jsonData: 17 + tracesToMetrics: 18 + datasourceUid: prometheus 19 + serviceMap: 20 + datasourceUid: prometheus
+7
dev/prometheus.yaml
··· 1 + global: 2 + scrape_interval: 15s 3 + 4 + scrape_configs: 5 + - job_name: arabica 6 + static_configs: 7 + - targets: ["host.docker.internal:9101"]
+40
dev/tempo.yaml
··· 1 + stream_over_http_enabled: true 2 + 3 + server: 4 + http_listen_port: 3200 5 + 6 + distributor: 7 + receivers: 8 + otlp: 9 + protocols: 10 + http: 11 + endpoint: "0.0.0.0:4318" 12 + 13 + storage: 14 + trace: 15 + backend: local 16 + local: 17 + path: /var/tempo/traces 18 + wal: 19 + path: /var/tempo/wal 20 + 21 + metrics_generator: 22 + processor: 23 + span_metrics: 24 + dimensions: 25 + - pds.method 26 + - pds.collection 27 + registry: 28 + external_labels: 29 + source: tempo 30 + storage: 31 + path: /var/tempo/generator/wal 32 + remote_write: 33 + - url: http://prometheus:9090/api/v1/write 34 + send_exemplars: true 35 + 36 + overrides: 37 + defaults: 38 + metrics_generator: 39 + processors: 40 + - span-metrics
+3 -3
flake.lock
··· 2 2 "nodes": { 3 3 "nixpkgs": { 4 4 "locked": { 5 - "lastModified": 1767026758, 6 - "narHash": "sha256-7fsac/f7nh/VaKJ/qm3I338+wAJa/3J57cOGpXi0Sbg=", 5 + "lastModified": 1772082373, 6 + "narHash": "sha256-wySf8a6hvuqgFdwvvzPPTARBCMLDz7WFAufGkllD1M4=", 7 7 "owner": "NixOS", 8 8 "repo": "nixpkgs", 9 - "rev": "346dd96ad74dc4457a9db9de4f4f57dab2e5731d", 9 + "rev": "26eaeac4e409d7b5a6bf6f90a2a2dc223c78d915", 10 10 "type": "github" 11 11 }, 12 12 "original": {
+21 -8
go.mod
··· 13 13 github.com/rs/zerolog v1.34.0 14 14 github.com/stretchr/testify v1.11.1 15 15 go.etcd.io/bbolt v1.3.8 16 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 17 + go.opentelemetry.io/otel v1.40.0 18 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 19 + go.opentelemetry.io/otel/sdk v1.40.0 20 + go.opentelemetry.io/otel/trace v1.40.0 16 21 golang.org/x/sync v0.19.0 17 22 modernc.org/sqlite v1.46.1 18 23 ) 19 24 20 25 require ( 21 26 github.com/beorn7/perks v1.0.1 // indirect 27 + github.com/cenkalti/backoff/v5 v5.0.3 // indirect 22 28 github.com/cespare/xxhash/v2 v2.3.0 // indirect 23 29 github.com/davecgh/go-spew v1.1.1 // indirect 24 30 github.com/dustin/go-humanize v1.0.1 // indirect 25 31 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 26 32 github.com/felixge/httpsnoop v1.0.4 // indirect 27 - github.com/go-logr/logr v1.4.1 // indirect 33 + github.com/go-logr/logr v1.4.3 // indirect 28 34 github.com/go-logr/stdr v1.2.2 // indirect 35 + github.com/go-logr/zerologr v1.2.3 // indirect 29 36 github.com/gogo/protobuf v1.3.2 // indirect 30 37 github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 31 38 github.com/google/uuid v1.6.0 // indirect 39 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect 32 40 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 33 41 github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 34 42 github.com/hashicorp/golang-lru v1.0.2 // indirect ··· 68 76 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 69 77 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 70 78 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 71 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 72 - go.opentelemetry.io/otel v1.21.0 // indirect 73 - go.opentelemetry.io/otel/metric v1.21.0 // indirect 74 - go.opentelemetry.io/otel/trace v1.21.0 // indirect 79 + go.opentelemetry.io/auto/sdk v1.2.1 // indirect 80 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect 81 + go.opentelemetry.io/otel/metric v1.40.0 // indirect 82 + go.opentelemetry.io/proto/otlp v1.9.0 // indirect 75 83 go.uber.org/atomic v1.11.0 // indirect 76 84 go.uber.org/multierr v1.11.0 // indirect 77 85 go.uber.org/zap v1.26.0 // indirect 78 86 go.yaml.in/yaml/v2 v2.4.2 // indirect 79 - golang.org/x/crypto v0.40.0 // indirect 87 + golang.org/x/crypto v0.47.0 // indirect 80 88 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect 81 - golang.org/x/sys v0.37.0 // indirect 89 + golang.org/x/net v0.49.0 // indirect 90 + golang.org/x/sys v0.40.0 // indirect 91 + golang.org/x/text v0.33.0 // indirect 82 92 golang.org/x/time v0.3.0 // indirect 83 93 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 84 - google.golang.org/protobuf v1.36.8 // indirect 94 + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect 95 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect 96 + google.golang.org/grpc v1.78.0 // indirect 97 + google.golang.org/protobuf v1.36.11 // indirect 85 98 gopkg.in/yaml.v3 v3.0.1 // indirect 86 99 lukechampine.com/blake3 v1.2.1 // indirect 87 100 modernc.org/libc v1.67.6 // indirect
+54 -22
go.sum
··· 6 6 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 7 github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725 h1:gfrLAhE6PHun4MDypO/5hpnaHPd9Dbe9+JxZL0gC4ic= 8 8 github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725/go.mod h1:KIy0FgNQacp4uv2Z7xhNkV3qZiUSGuRky97s7Pa4v+o= 9 + github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 10 + github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 9 11 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 12 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 13 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= ··· 20 22 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 21 23 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 22 24 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 23 - github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 24 - github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 25 + github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 26 + github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 25 27 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 26 28 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 29 + github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= 30 + github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho= 27 31 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 28 32 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 29 33 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 30 34 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 31 35 github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 32 36 github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 37 + github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 38 + github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 33 39 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 34 40 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 35 41 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= ··· 44 50 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 45 51 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 46 52 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 53 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= 54 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= 47 55 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 48 56 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 49 57 github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= ··· 145 153 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 146 154 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 147 155 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 148 - github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 149 - github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 156 + github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 157 + github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 150 158 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 151 159 github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 152 160 github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= ··· 179 187 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 180 188 go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= 181 189 go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= 182 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 183 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 184 - go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= 185 - go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 186 - go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= 187 - go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 188 - go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= 189 - go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 190 + go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 191 + go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 192 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= 193 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= 194 + go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= 195 + go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= 196 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= 197 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= 198 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= 199 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= 200 + go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= 201 + go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= 202 + go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= 203 + go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= 204 + go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= 205 + go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= 206 + go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= 207 + go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= 208 + go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= 209 + go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= 190 210 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 191 211 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 192 212 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 209 229 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 210 230 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 211 231 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 212 - golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= 213 - golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 232 + golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= 233 + golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 214 234 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= 215 235 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 216 236 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= ··· 218 238 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 219 239 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 220 240 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 221 - golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= 222 - golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 241 + golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= 242 + golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 223 243 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 224 244 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 225 245 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 226 246 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 227 247 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 228 248 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 249 + golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= 250 + golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= 229 251 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 230 252 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 231 253 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 243 265 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 244 266 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 267 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 246 - golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 247 - golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 268 + golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= 269 + golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 248 270 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 249 271 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 250 272 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 273 + golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= 274 + golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= 251 275 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 252 276 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 253 277 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 260 284 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 261 285 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 262 286 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 263 - golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 264 - golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 287 + golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= 288 + golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 265 289 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 266 290 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 267 291 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 268 292 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 269 293 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 270 294 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 271 - google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 272 - google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 295 + gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 296 + gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 297 + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= 298 + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= 299 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= 300 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= 301 + google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= 302 + google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= 303 + google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 304 + google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 273 305 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 274 306 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 275 307 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+27 -41
internal/atproto/client.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 - "time" 7 6 8 7 "arabica/internal/metrics" 8 + "arabica/internal/tracing" 9 9 10 10 "github.com/bluesky-social/indigo/atproto/atclient" 11 11 "github.com/bluesky-social/indigo/atproto/syntax" ··· 55 55 56 56 // CreateRecord creates a new record in the user's repository 57 57 func (c *Client) CreateRecord(ctx context.Context, did syntax.DID, sessionID string, input *CreateRecordInput) (*CreateRecordOutput, error) { 58 - start := time.Now() 58 + ctx, span := tracing.PdsSpan(ctx, "createRecord", input.Collection, did.String()) 59 + defer span.End() 59 60 60 61 apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 61 62 if err != nil { 63 + tracing.EndWithError(span, err) 62 64 return nil, err 63 65 } 64 66 ··· 80 82 } 81 83 82 84 err = apiClient.Post(ctx, "com.atproto.repo.createRecord", body, &result) 83 - 84 - duration := time.Since(start) 85 - metrics.PDSRequestDuration.WithLabelValues("createRecord").Observe(duration.Seconds()) 86 85 metrics.PDSRequestsTotal.WithLabelValues("createRecord", input.Collection).Inc() 87 86 88 87 if err != nil { 89 - metrics.PDSErrorsTotal.WithLabelValues("createRecord").Inc() 88 + tracing.EndWithError(span, err) 90 89 log.Error(). 91 90 Err(err). 92 91 Str("method", "createRecord"). 93 92 Str("collection", input.Collection). 94 93 Str("did", did.String()). 95 - Dur("duration", duration). 96 94 Msg("PDS request failed") 97 95 return nil, fmt.Errorf("failed to create record: %w", err) 98 96 } ··· 103 101 Str("did", did.String()). 104 102 Str("uri", result.URI). 105 103 Str("cid", result.CID). 106 - Dur("duration", duration). 107 104 Msg("PDS request completed") 108 105 109 106 return &CreateRecordOutput{ ··· 127 124 128 125 // GetRecord retrieves a single record by its rkey 129 126 func (c *Client) GetRecord(ctx context.Context, did syntax.DID, sessionID string, input *GetRecordInput) (*GetRecordOutput, error) { 130 - start := time.Now() 127 + ctx, span := tracing.PdsSpan(ctx, "getRecord", input.Collection, did.String()) 128 + defer span.End() 131 129 132 130 apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 133 131 if err != nil { 132 + tracing.EndWithError(span, err) 134 133 return nil, err 135 134 } 136 135 ··· 149 148 } 150 149 151 150 err = apiClient.Get(ctx, "com.atproto.repo.getRecord", params, &result) 152 - 153 - duration := time.Since(start) 154 - metrics.PDSRequestDuration.WithLabelValues("getRecord").Observe(duration.Seconds()) 155 151 metrics.PDSRequestsTotal.WithLabelValues("getRecord", input.Collection).Inc() 156 152 157 153 if err != nil { 158 - metrics.PDSErrorsTotal.WithLabelValues("getRecord").Inc() 154 + tracing.EndWithError(span, err) 159 155 log.Error(). 160 156 Err(err). 161 157 Str("method", "getRecord"). 162 158 Str("collection", input.Collection). 163 159 Str("rkey", input.RKey). 164 160 Str("did", did.String()). 165 - Dur("duration", duration). 166 161 Msg("PDS request failed") 167 162 return nil, fmt.Errorf("failed to get record: %w", err) 168 163 } ··· 174 169 Str("did", did.String()). 175 170 Str("uri", result.URI). 176 171 Str("cid", result.CID). 177 - Dur("duration", duration). 178 172 Msg("PDS request completed") 179 173 180 174 return &GetRecordOutput{ ··· 206 200 207 201 // ListRecords retrieves a list of records from a collection 208 202 func (c *Client) ListRecords(ctx context.Context, did syntax.DID, sessionID string, input *ListRecordsInput) (*ListRecordsOutput, error) { 209 - start := time.Now() 203 + ctx, span := tracing.PdsSpan(ctx, "listRecords", input.Collection, did.String()) 204 + defer span.End() 210 205 211 206 apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 212 207 if err != nil { 208 + tracing.EndWithError(span, err) 213 209 return nil, err 214 210 } 215 211 ··· 237 233 } 238 234 239 235 err = apiClient.Get(ctx, "com.atproto.repo.listRecords", params, &result) 240 - 241 - duration := time.Since(start) 242 236 recordCount := len(result.Records) 243 - metrics.PDSRequestDuration.WithLabelValues("listRecords").Observe(duration.Seconds()) 244 237 metrics.PDSRequestsTotal.WithLabelValues("listRecords", input.Collection).Inc() 245 238 246 239 if err != nil { 247 - metrics.PDSErrorsTotal.WithLabelValues("listRecords").Inc() 240 + tracing.EndWithError(span, err) 248 241 log.Error(). 249 242 Err(err). 250 243 Str("method", "listRecords"). 251 244 Str("collection", input.Collection). 252 245 Str("did", did.String()). 253 - Dur("duration", duration). 254 246 Msg("PDS request failed") 255 247 return nil, fmt.Errorf("failed to list records: %w", err) 256 248 } ··· 259 251 Str("method", "listRecords"). 260 252 Str("collection", input.Collection). 261 253 Str("did", did.String()). 262 - Int("record_count", recordCount). 263 - Dur("duration", duration) 254 + Int("record_count", recordCount) 264 255 265 256 if result.Cursor != nil && *result.Cursor != "" { 266 257 logEvent.Str("cursor", *result.Cursor).Bool("has_more", true) ··· 289 280 // ListAllRecords retrieves all records from a collection, handling pagination automatically 290 281 // This is useful when you need to fetch the complete collection without worrying about pagination 291 282 func (c *Client) ListAllRecords(ctx context.Context, did syntax.DID, sessionID string, collection string) (*ListRecordsOutput, error) { 292 - start := time.Now() 283 + ctx, span := tracing.PdsSpan(ctx, "listAllRecords", collection, did.String()) 284 + defer span.End() 285 + 293 286 var allRecords []Record 294 287 var cursor *string 295 288 pageCount := 0 ··· 303 296 // This allows long-running pagination to be cancelled gracefully 304 297 select { 305 298 case <-ctx.Done(): 299 + tracing.EndWithError(span, ctx.Err()) 306 300 return nil, ctx.Err() 307 301 default: 308 302 } ··· 313 307 Cursor: cursor, 314 308 }) 315 309 if err != nil { 310 + tracing.EndWithError(span, err) 316 311 return nil, err 317 312 } 318 313 ··· 327 322 cursor = output.Cursor 328 323 } 329 324 330 - duration := time.Since(start) 331 - 332 325 log.Info(). 333 326 Str("method", "listAllRecords"). 334 327 Str("collection", collection). 335 328 Str("did", did.String()). 336 329 Int("total_records", len(allRecords)). 337 330 Int("pages_fetched", pageCount). 338 - Dur("duration", duration). 339 331 Msg("PDS pagination completed") 340 332 341 333 return &ListRecordsOutput{ ··· 353 345 354 346 // PutRecord updates an existing record in the user's repository 355 347 func (c *Client) PutRecord(ctx context.Context, did syntax.DID, sessionID string, input *PutRecordInput) error { 356 - start := time.Now() 348 + ctx, span := tracing.PdsSpan(ctx, "putRecord", input.Collection, did.String()) 349 + defer span.End() 357 350 358 351 apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 359 352 if err != nil { 353 + tracing.EndWithError(span, err) 360 354 return err 361 355 } 362 356 ··· 375 369 } 376 370 377 371 err = apiClient.Post(ctx, "com.atproto.repo.putRecord", body, &result) 378 - 379 - duration := time.Since(start) 380 - metrics.PDSRequestDuration.WithLabelValues("putRecord").Observe(duration.Seconds()) 381 372 metrics.PDSRequestsTotal.WithLabelValues("putRecord", input.Collection).Inc() 382 373 383 374 if err != nil { 384 - metrics.PDSErrorsTotal.WithLabelValues("putRecord").Inc() 375 + tracing.EndWithError(span, err) 385 376 log.Error(). 386 377 Err(err). 387 378 Str("method", "putRecord"). 388 379 Str("collection", input.Collection). 389 380 Str("rkey", input.RKey). 390 381 Str("did", did.String()). 391 - Dur("duration", duration). 392 382 Msg("PDS request failed") 393 383 return fmt.Errorf("failed to update record: %w", err) 394 384 } ··· 400 390 Str("did", did.String()). 401 391 Str("uri", result.URI). 402 392 Str("cid", result.CID). 403 - Dur("duration", duration). 404 393 Msg("PDS request completed") 405 394 406 395 return nil ··· 414 403 415 404 // DeleteRecord deletes a record from the user's repository 416 405 func (c *Client) DeleteRecord(ctx context.Context, did syntax.DID, sessionID string, input *DeleteRecordInput) error { 417 - start := time.Now() 406 + ctx, span := tracing.PdsSpan(ctx, "deleteRecord", input.Collection, did.String()) 407 + defer span.End() 418 408 419 409 apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 420 410 if err != nil { 411 + tracing.EndWithError(span, err) 421 412 return err 422 413 } 423 414 ··· 432 423 var result struct{} 433 424 434 425 err = apiClient.Post(ctx, "com.atproto.repo.deleteRecord", body, &result) 435 - 436 - duration := time.Since(start) 437 - metrics.PDSRequestDuration.WithLabelValues("deleteRecord").Observe(duration.Seconds()) 438 426 metrics.PDSRequestsTotal.WithLabelValues("deleteRecord", input.Collection).Inc() 439 427 440 428 if err != nil { 441 - metrics.PDSErrorsTotal.WithLabelValues("deleteRecord").Inc() 429 + tracing.EndWithError(span, err) 442 430 log.Error(). 443 431 Err(err). 444 432 Str("method", "deleteRecord"). 445 433 Str("collection", input.Collection). 446 434 Str("rkey", input.RKey). 447 435 Str("did", did.String()). 448 - Dur("duration", duration). 449 436 Msg("PDS request failed") 450 437 return fmt.Errorf("failed to delete record: %w", err) 451 438 } ··· 455 442 Str("collection", input.Collection). 456 443 Str("rkey", input.RKey). 457 444 Str("did", did.String()). 458 - Dur("duration", duration). 459 445 Msg("PDS request completed") 460 446 461 447 return nil
-11
internal/metrics/metrics.go
··· 43 43 Name: "arabica_pds_requests_total", 44 44 Help: "Total number of PDS requests", 45 45 }, []string{"method", "collection"}) 46 - 47 - PDSRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ 48 - Name: "arabica_pds_request_duration_seconds", 49 - Help: "PDS request duration in seconds", 50 - Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10}, 51 - }, []string{"method"}) 52 - 53 - PDSErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ 54 - Name: "arabica_pds_errors_total", 55 - Help: "Total number of PDS request errors", 56 - }, []string{"method"}) 57 46 ) 58 47 59 48 // Feed metrics
+5 -1
internal/routing/routing.go
··· 8 8 "arabica/internal/middleware" 9 9 10 10 "github.com/rs/zerolog" 11 + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 11 12 ) 12 13 13 14 // Config holds the configuration needed for setting up routes ··· 158 159 // 4. Apply security headers 159 160 handler = middleware.SecurityHeadersMiddleware(handler) 160 161 161 - // 5. Apply logging middleware (outermost - wraps everything) 162 + // 5. Apply logging middleware 162 163 handler = middleware.LoggingMiddleware(cfg.Logger)(handler) 164 + 165 + // 6. Apply OpenTelemetry HTTP instrumentation (outermost - wraps everything) 166 + handler = otelhttp.NewHandler(handler, "arabica") 163 167 164 168 return handler 165 169 }
+78
internal/tracing/tracing.go
··· 1 + package tracing 2 + 3 + import ( 4 + "context" 5 + "os" 6 + 7 + "github.com/go-logr/zerologr" 8 + "github.com/rs/zerolog/log" 9 + "go.opentelemetry.io/otel" 10 + "go.opentelemetry.io/otel/attribute" 11 + "go.opentelemetry.io/otel/codes" 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.26.0" 17 + "go.opentelemetry.io/otel/trace" 18 + ) 19 + 20 + // tracer returns the package tracer. This must be a function (not a package-level var) 21 + // because the global TracerProvider isn't set until Init() runs. 22 + func tracer() trace.Tracer { 23 + return otel.Tracer("arabica") 24 + } 25 + 26 + // Init creates and registers a tracer provider with an OTLP HTTP exporter. 27 + // It reads OTEL_EXPORTER_OTLP_ENDPOINT (default: localhost:4318). 28 + // Returns the provider so the caller can defer Shutdown. 29 + func Init(ctx context.Context) (*sdktrace.TracerProvider, error) { 30 + // Bridge OTel's internal logger to zerolog 31 + otel.SetLogger(zerologr.New(&log.Logger)) 32 + 33 + endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") 34 + if endpoint == "" { 35 + endpoint = "localhost:4318" 36 + } 37 + 38 + exp, err := otlptracehttp.New(ctx, 39 + otlptracehttp.WithEndpoint(endpoint), 40 + otlptracehttp.WithInsecure(), 41 + ) 42 + if err != nil { 43 + return nil, err 44 + } 45 + 46 + tp := sdktrace.NewTracerProvider( 47 + sdktrace.WithBatcher(exp), 48 + sdktrace.WithResource(resource.NewWithAttributes( 49 + semconv.SchemaURL, 50 + semconv.ServiceNameKey.String("arabica"), 51 + )), 52 + ) 53 + 54 + otel.SetTracerProvider(tp) 55 + otel.SetTextMapPropagator(propagation.TraceContext{}) 56 + 57 + return tp, nil 58 + } 59 + 60 + // PdsSpan starts a span for a PDS operation with standard attributes. 61 + func PdsSpan(ctx context.Context, method, collection, did string) (context.Context, trace.Span) { 62 + return tracer().Start(ctx, "pds."+method, 63 + trace.WithAttributes( 64 + attribute.String("pds.method", method), 65 + attribute.String("pds.collection", collection), 66 + attribute.String("pds.did", did), 67 + ), 68 + ) 69 + } 70 + 71 + // EndWithError records an error on a span and sets its status. 72 + // If err is nil, this is a no-op. 73 + func EndWithError(span trace.Span, err error) { 74 + if err != nil { 75 + span.RecordError(err) 76 + span.SetStatus(codes.Error, err.Error()) 77 + } 78 + }
+10
module.nix
··· 218 218 description = "Group under which arabica runs."; 219 219 }; 220 220 221 + otelEndpoint = lib.mkOption { 222 + type = lib.types.nullOr lib.types.str; 223 + default = null; 224 + description = 225 + "OTLP HTTP endpoint for OpenTelemetry traces (e.g. localhost:4318)."; 226 + example = "localhost:4318"; 227 + }; 228 + 221 229 openFirewall = lib.mkOption { 222 230 type = lib.types.bool; 223 231 default = false; ··· 286 294 SMTP_PORT = toString cfg.smtp.port; 287 295 } // lib.optionalAttrs (cfg.smtp.enable && cfg.smtp.from != "") { 288 296 SMTP_FROM = cfg.smtp.from; 297 + } // lib.optionalAttrs (cfg.otelEndpoint != null) { 298 + OTEL_EXPORTER_OTLP_ENDPOINT = cfg.otelEndpoint; 289 299 }; 290 300 }; 291 301