tangled
alpha
login
or
join now
arabica.social
/
arabica
7
fork
atom
Coffee journaling on ATProto (alpha)
alpha.arabica.social
coffee
7
fork
atom
overview
issues
pulls
pipelines
feat: otel tracing
pdewey.com
1 week ago
5960d84a
9b52a8ea
verified
This commit was signed with the committer's
known signature
.
pdewey.com
SSH Key Fingerprint:
SHA256:ePOVkJstqVLchGK8m9/OGQG+aFNHD5XN3xjvW9wKCA4=
+337
-86
14 changed files
expand all
collapse all
unified
split
CLAUDE.md
cmd
server
main.go
dev
docker-compose.yaml
grafana-datasources.yaml
prometheus.yaml
tempo.yaml
flake.lock
go.mod
go.sum
internal
atproto
client.go
metrics
metrics.go
routing
routing.go
tracing
tracing.go
module.nix
+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
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
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
68
+
69
69
+
// Initialize OpenTelemetry tracing
70
70
+
tp, err := tracing.Init(context.Background())
71
71
+
if err != nil {
72
72
+
log.Warn().Err(err).Msg("Failed to initialize tracing, continuing without it")
73
73
+
} else {
74
74
+
defer func() {
75
75
+
if err := tp.Shutdown(context.Background()); err != nil {
76
76
+
log.Error().Err(err).Msg("Error shutting down tracer provider")
77
77
+
}
78
78
+
}()
79
79
+
log.Info().Msg("OpenTelemetry tracing initialized")
80
80
+
}
67
81
68
82
// Get port from env or use default
69
83
port := os.Getenv("PORT")
+57
dev/docker-compose.yaml
···
1
1
+
# Local dev support: Grafana + Tempo + Prometheus
2
2
+
# Run with: docker compose -f dev/docker-compose.yaml up
3
3
+
#
4
4
+
# Services:
5
5
+
# Grafana: http://localhost:3000 (admin/admin)
6
6
+
# Tempo: http://localhost:3200 (OTLP ingest on :4318)
7
7
+
# Prometheus: http://localhost:9090 (scrapes arabica metrics on :9101)
8
8
+
#
9
9
+
# Arabica will send traces to localhost:4318 by default (no config needed).
10
10
+
11
11
+
services:
12
12
+
tempo:
13
13
+
image: grafana/tempo:2.7.2
14
14
+
volumes:
15
15
+
- ./tempo.yaml:/etc/tempo.yaml:ro
16
16
+
- tempo-data:/var/tempo
17
17
+
command: ["-config.file=/etc/tempo.yaml"]
18
18
+
ports:
19
19
+
- "4318:4318" # OTLP HTTP receiver
20
20
+
- "3200:3200" # Tempo API
21
21
+
extra_hosts:
22
22
+
- "host.docker.internal:host-gateway"
23
23
+
24
24
+
prometheus:
25
25
+
image: prom/prometheus:v3.4.0
26
26
+
volumes:
27
27
+
- ./prometheus.yaml:/etc/prometheus/prometheus.yml:ro
28
28
+
- prometheus-data:/prometheus
29
29
+
command:
30
30
+
- "--config.file=/etc/prometheus/prometheus.yml"
31
31
+
- "--web.enable-remote-write-receiver"
32
32
+
- "--storage.tsdb.path=/prometheus"
33
33
+
ports:
34
34
+
- "9090:9090"
35
35
+
extra_hosts:
36
36
+
- "host.docker.internal:host-gateway"
37
37
+
38
38
+
grafana:
39
39
+
image: grafana/grafana:11.6.1
40
40
+
volumes:
41
41
+
- ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml:ro
42
42
+
- ../grafana:/var/lib/grafana/dashboards:ro
43
43
+
- grafana-data:/var/lib/grafana
44
44
+
environment:
45
45
+
GF_AUTH_ANONYMOUS_ENABLED: "true"
46
46
+
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
47
47
+
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /var/lib/grafana/dashboards/arabica-prometheus.json
48
48
+
ports:
49
49
+
- "3000:3000"
50
50
+
depends_on:
51
51
+
- tempo
52
52
+
- prometheus
53
53
+
54
54
+
volumes:
55
55
+
tempo-data:
56
56
+
prometheus-data:
57
57
+
grafana-data:
+20
dev/grafana-datasources.yaml
···
1
1
+
apiVersion: 1
2
2
+
3
3
+
datasources:
4
4
+
- name: Prometheus
5
5
+
type: prometheus
6
6
+
access: proxy
7
7
+
url: http://prometheus:9090
8
8
+
isDefault: true
9
9
+
editable: true
10
10
+
11
11
+
- name: Tempo
12
12
+
type: tempo
13
13
+
access: proxy
14
14
+
url: http://tempo:3200
15
15
+
editable: true
16
16
+
jsonData:
17
17
+
tracesToMetrics:
18
18
+
datasourceUid: prometheus
19
19
+
serviceMap:
20
20
+
datasourceUid: prometheus
+7
dev/prometheus.yaml
···
1
1
+
global:
2
2
+
scrape_interval: 15s
3
3
+
4
4
+
scrape_configs:
5
5
+
- job_name: arabica
6
6
+
static_configs:
7
7
+
- targets: ["host.docker.internal:9101"]
+40
dev/tempo.yaml
···
1
1
+
stream_over_http_enabled: true
2
2
+
3
3
+
server:
4
4
+
http_listen_port: 3200
5
5
+
6
6
+
distributor:
7
7
+
receivers:
8
8
+
otlp:
9
9
+
protocols:
10
10
+
http:
11
11
+
endpoint: "0.0.0.0:4318"
12
12
+
13
13
+
storage:
14
14
+
trace:
15
15
+
backend: local
16
16
+
local:
17
17
+
path: /var/tempo/traces
18
18
+
wal:
19
19
+
path: /var/tempo/wal
20
20
+
21
21
+
metrics_generator:
22
22
+
processor:
23
23
+
span_metrics:
24
24
+
dimensions:
25
25
+
- pds.method
26
26
+
- pds.collection
27
27
+
registry:
28
28
+
external_labels:
29
29
+
source: tempo
30
30
+
storage:
31
31
+
path: /var/tempo/generator/wal
32
32
+
remote_write:
33
33
+
- url: http://prometheus:9090/api/v1/write
34
34
+
send_exemplars: true
35
35
+
36
36
+
overrides:
37
37
+
defaults:
38
38
+
metrics_generator:
39
39
+
processors:
40
40
+
- span-metrics
+3
-3
flake.lock
···
2
2
"nodes": {
3
3
"nixpkgs": {
4
4
"locked": {
5
5
-
"lastModified": 1767026758,
6
6
-
"narHash": "sha256-7fsac/f7nh/VaKJ/qm3I338+wAJa/3J57cOGpXi0Sbg=",
5
5
+
"lastModified": 1772082373,
6
6
+
"narHash": "sha256-wySf8a6hvuqgFdwvvzPPTARBCMLDz7WFAufGkllD1M4=",
7
7
"owner": "NixOS",
8
8
"repo": "nixpkgs",
9
9
-
"rev": "346dd96ad74dc4457a9db9de4f4f57dab2e5731d",
9
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
16
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
17
17
+
go.opentelemetry.io/otel v1.40.0
18
18
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0
19
19
+
go.opentelemetry.io/otel/sdk v1.40.0
20
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
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
27
-
github.com/go-logr/logr v1.4.1 // indirect
33
33
+
github.com/go-logr/logr v1.4.3 // indirect
28
34
github.com/go-logr/stdr v1.2.2 // indirect
35
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
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
71
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
72
72
-
go.opentelemetry.io/otel v1.21.0 // indirect
73
73
-
go.opentelemetry.io/otel/metric v1.21.0 // indirect
74
74
-
go.opentelemetry.io/otel/trace v1.21.0 // indirect
79
79
+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
80
80
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
81
81
+
go.opentelemetry.io/otel/metric v1.40.0 // indirect
82
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
79
-
golang.org/x/crypto v0.40.0 // indirect
87
87
+
golang.org/x/crypto v0.47.0 // indirect
80
88
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
81
81
-
golang.org/x/sys v0.37.0 // indirect
89
89
+
golang.org/x/net v0.49.0 // indirect
90
90
+
golang.org/x/sys v0.40.0 // indirect
91
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
84
-
google.golang.org/protobuf v1.36.8 // indirect
94
94
+
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
95
95
+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
96
96
+
google.golang.org/grpc v1.78.0 // indirect
97
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
9
+
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
10
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
23
-
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
24
24
-
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
25
25
+
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
26
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
29
+
github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=
30
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
37
+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
38
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
53
+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
54
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
148
-
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
149
149
-
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
156
156
+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
157
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
182
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
183
183
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
184
184
-
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
185
185
-
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
186
186
-
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
187
187
-
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
188
188
-
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
189
189
-
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
190
190
+
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
191
191
+
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
192
192
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
193
193
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
194
194
+
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
195
195
+
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
196
196
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
197
197
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
198
198
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
199
199
+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
200
200
+
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
201
201
+
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
202
202
+
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
203
203
+
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
204
204
+
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
205
205
+
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
206
206
+
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
207
207
+
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
208
208
+
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
209
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
212
-
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
213
213
-
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
232
232
+
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
233
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
221
-
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
222
222
-
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
241
241
+
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
242
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
249
+
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
250
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
246
-
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
247
247
-
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
268
268
+
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
269
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
273
+
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
274
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
263
-
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
264
264
-
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
287
287
+
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
288
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
271
-
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
272
272
-
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
295
295
+
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
296
296
+
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
297
297
+
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
298
298
+
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
299
299
+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
300
300
+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
301
301
+
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
302
302
+
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
303
303
+
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
304
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
6
-
"time"
7
6
8
7
"arabica/internal/metrics"
8
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
58
-
start := time.Now()
58
58
+
ctx, span := tracing.PdsSpan(ctx, "createRecord", input.Collection, did.String())
59
59
+
defer span.End()
59
60
60
61
apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID)
61
62
if err != nil {
63
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
83
-
84
84
-
duration := time.Since(start)
85
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
89
-
metrics.PDSErrorsTotal.WithLabelValues("createRecord").Inc()
88
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
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
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
130
-
start := time.Now()
127
127
+
ctx, span := tracing.PdsSpan(ctx, "getRecord", input.Collection, did.String())
128
128
+
defer span.End()
131
129
132
130
apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID)
133
131
if err != nil {
132
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
152
-
153
153
-
duration := time.Since(start)
154
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
158
-
metrics.PDSErrorsTotal.WithLabelValues("getRecord").Inc()
154
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
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
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
209
-
start := time.Now()
203
203
+
ctx, span := tracing.PdsSpan(ctx, "listRecords", input.Collection, did.String())
204
204
+
defer span.End()
210
205
211
206
apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID)
212
207
if err != nil {
208
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
240
-
241
241
-
duration := time.Since(start)
242
236
recordCount := len(result.Records)
243
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
247
-
metrics.PDSErrorsTotal.WithLabelValues("listRecords").Inc()
240
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
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
262
-
Int("record_count", recordCount).
263
263
-
Dur("duration", duration)
254
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
292
-
start := time.Now()
283
283
+
ctx, span := tracing.PdsSpan(ctx, "listAllRecords", collection, did.String())
284
284
+
defer span.End()
285
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
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
310
+
tracing.EndWithError(span, err)
316
311
return nil, err
317
312
}
318
313
···
327
322
cursor = output.Cursor
328
323
}
329
324
330
330
-
duration := time.Since(start)
331
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
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
356
-
start := time.Now()
348
348
+
ctx, span := tracing.PdsSpan(ctx, "putRecord", input.Collection, did.String())
349
349
+
defer span.End()
357
350
358
351
apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID)
359
352
if err != nil {
353
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
378
-
379
379
-
duration := time.Since(start)
380
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
384
-
metrics.PDSErrorsTotal.WithLabelValues("putRecord").Inc()
375
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
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
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
417
-
start := time.Now()
406
406
+
ctx, span := tracing.PdsSpan(ctx, "deleteRecord", input.Collection, did.String())
407
407
+
defer span.End()
418
408
419
409
apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID)
420
410
if err != nil {
411
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
435
-
436
436
-
duration := time.Since(start)
437
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
441
-
metrics.PDSErrorsTotal.WithLabelValues("deleteRecord").Inc()
429
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
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
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
46
-
47
47
-
PDSRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
48
48
-
Name: "arabica_pds_request_duration_seconds",
49
49
-
Help: "PDS request duration in seconds",
50
50
-
Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
51
51
-
}, []string{"method"})
52
52
-
53
53
-
PDSErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
54
54
-
Name: "arabica_pds_errors_total",
55
55
-
Help: "Total number of PDS request errors",
56
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
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
161
-
// 5. Apply logging middleware (outermost - wraps everything)
162
162
+
// 5. Apply logging middleware
162
163
handler = middleware.LoggingMiddleware(cfg.Logger)(handler)
164
164
+
165
165
+
// 6. Apply OpenTelemetry HTTP instrumentation (outermost - wraps everything)
166
166
+
handler = otelhttp.NewHandler(handler, "arabica")
163
167
164
168
return handler
165
169
}
+78
internal/tracing/tracing.go
···
1
1
+
package tracing
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"os"
6
6
+
7
7
+
"github.com/go-logr/zerologr"
8
8
+
"github.com/rs/zerolog/log"
9
9
+
"go.opentelemetry.io/otel"
10
10
+
"go.opentelemetry.io/otel/attribute"
11
11
+
"go.opentelemetry.io/otel/codes"
12
12
+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
13
13
+
"go.opentelemetry.io/otel/propagation"
14
14
+
"go.opentelemetry.io/otel/sdk/resource"
15
15
+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
16
16
+
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
17
17
+
"go.opentelemetry.io/otel/trace"
18
18
+
)
19
19
+
20
20
+
// tracer returns the package tracer. This must be a function (not a package-level var)
21
21
+
// because the global TracerProvider isn't set until Init() runs.
22
22
+
func tracer() trace.Tracer {
23
23
+
return otel.Tracer("arabica")
24
24
+
}
25
25
+
26
26
+
// Init creates and registers a tracer provider with an OTLP HTTP exporter.
27
27
+
// It reads OTEL_EXPORTER_OTLP_ENDPOINT (default: localhost:4318).
28
28
+
// Returns the provider so the caller can defer Shutdown.
29
29
+
func Init(ctx context.Context) (*sdktrace.TracerProvider, error) {
30
30
+
// Bridge OTel's internal logger to zerolog
31
31
+
otel.SetLogger(zerologr.New(&log.Logger))
32
32
+
33
33
+
endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
34
34
+
if endpoint == "" {
35
35
+
endpoint = "localhost:4318"
36
36
+
}
37
37
+
38
38
+
exp, err := otlptracehttp.New(ctx,
39
39
+
otlptracehttp.WithEndpoint(endpoint),
40
40
+
otlptracehttp.WithInsecure(),
41
41
+
)
42
42
+
if err != nil {
43
43
+
return nil, err
44
44
+
}
45
45
+
46
46
+
tp := sdktrace.NewTracerProvider(
47
47
+
sdktrace.WithBatcher(exp),
48
48
+
sdktrace.WithResource(resource.NewWithAttributes(
49
49
+
semconv.SchemaURL,
50
50
+
semconv.ServiceNameKey.String("arabica"),
51
51
+
)),
52
52
+
)
53
53
+
54
54
+
otel.SetTracerProvider(tp)
55
55
+
otel.SetTextMapPropagator(propagation.TraceContext{})
56
56
+
57
57
+
return tp, nil
58
58
+
}
59
59
+
60
60
+
// PdsSpan starts a span for a PDS operation with standard attributes.
61
61
+
func PdsSpan(ctx context.Context, method, collection, did string) (context.Context, trace.Span) {
62
62
+
return tracer().Start(ctx, "pds."+method,
63
63
+
trace.WithAttributes(
64
64
+
attribute.String("pds.method", method),
65
65
+
attribute.String("pds.collection", collection),
66
66
+
attribute.String("pds.did", did),
67
67
+
),
68
68
+
)
69
69
+
}
70
70
+
71
71
+
// EndWithError records an error on a span and sets its status.
72
72
+
// If err is nil, this is a no-op.
73
73
+
func EndWithError(span trace.Span, err error) {
74
74
+
if err != nil {
75
75
+
span.RecordError(err)
76
76
+
span.SetStatus(codes.Error, err.Error())
77
77
+
}
78
78
+
}
+10
module.nix
···
218
218
description = "Group under which arabica runs.";
219
219
};
220
220
221
221
+
otelEndpoint = lib.mkOption {
222
222
+
type = lib.types.nullOr lib.types.str;
223
223
+
default = null;
224
224
+
description =
225
225
+
"OTLP HTTP endpoint for OpenTelemetry traces (e.g. localhost:4318).";
226
226
+
example = "localhost:4318";
227
227
+
};
228
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
297
+
} // lib.optionalAttrs (cfg.otelEndpoint != null) {
298
298
+
OTEL_EXPORTER_OTLP_ENDPOINT = cfg.otelEndpoint;
289
299
};
290
300
};
291
301