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

feat: stats page and metrics

pdewey.com decf5a14 9922f628

verified
+2513 -31
+1
.cells/cells.jsonl
··· 16 {"id":"01KGDXVR86CB2H44DJHKN4Z9M0","title":"Implement comments for social interactions","description":"Add a comment lexicon and implement comment functionality across the app.\n\n## Lexicon: social.arabica.alpha.comment\n\nThe comment record should:\n- Use `com.atproto.repo.strongRef` for the subject (URI + CID for immutability)\n- Support commenting on any arabica.social lexicon type (beans, roasters, grinders, brewers, brews, and other comments)\n- Include text field (max 1000 chars / 300 graphemes as per AT Protocol conventions)\n- Include createdAt timestamp\n- Use TID as record key\n- NOTE: This cell implements flat comments only. Threaded/nested comments are handled in a separate cell.\n\n## Backend Implementation\n\n1. Create lexicon file: `lexicons/social.arabica.alpha.comment.json`\n2. Add NSID constant in `internal/atproto/nsid.go`\n3. Add Comment model in `internal/models/models.go`\n4. Add record conversion functions in `internal/atproto/records.go`\n5. Add Store interface methods: CreateComment, DeleteComment, GetCommentsForSubject, GetUserComments\n6. Implement in AtprotoStore\n7. Update firehose indexing to track comments\n\n## Frontend Implementation\n\n1. Add comment section component below brew detail view\n2. Add comment form (authenticated users only)\n3. Display comment count on records in feed\n4. Update comment counts in response to firehose events\n5. Show commenter profile info (avatar, handle)\n\n## Acceptance Criteria\n\n- Users can comment on any arabica.social record\n- Comments are stored in the user's PDS (actor-owned data)\n- Comment counts display on records in the feed\n- Comments visible on record detail views\n- Real-time comment count updates via firehose","status":"open","priority":"normal","labels":["backend","frontend","atproto"],"created_at":"2026-02-02T01:00:51.846427126Z","updated_at":"2026-02-02T01:00:51.846427126Z"} 17 {"id":"01KGDXW31PHFMBE3WTV83HPET1","title":"Implement comment threading","description":"Add support for threaded/nested comments, building on the flat comment system.\n\n## Lexicon Changes\n\nUpdate `social.arabica.alpha.comment` to add optional threading fields:\n- `parent`: Optional strongRef to parent comment (for replies)\n- `root`: Optional strongRef to root subject (maintains context when replying to comments)\n\nAlternatively, consider a separate reply field or keeping comments flat with UI-level threading.\n\n## Backend Implementation\n\n1. Update comment model to include parent/root references\n2. Add methods to fetch comment threads: GetCommentThread, GetReplies\n3. Update firehose indexing to track parent-child relationships\n4. Add depth limits for threading (prevent infinite nesting)\n\n## Frontend Implementation\n\n1. Design threaded comment UI (indentation, collapse/expand)\n2. Add 'reply' button on comments\n3. Show reply context when replying\n4. Consider max nesting depth for display (e.g., 3-4 levels)\n5. Mobile-friendly thread navigation\n\n## Design Considerations\n\n- How deep should threading go? (Recommend max 3-4 levels visible, then flatten)\n- How to handle deleted parent comments?\n- Should users be notified when someone replies to their comment?\n- Performance: lazy-load deep threads vs eager-load\n\n## Acceptance Criteria\n\n- Users can reply directly to comments\n- Thread structure is visually clear\n- Threads can be collapsed/expanded\n- Works well on mobile devices\n- Parent context shown when replying","status":"blocked","priority":"low","blocked_by":["01KGDXVR86CB2H44DJHKN4Z9M0"],"labels":["frontend","atproto"],"created_at":"2026-02-02T01:01:02.902692829Z","updated_at":"2026-02-02T01:01:06.361891137Z"} 18 {"id":"01KJ36P2BWM4HSZV6GNX0BJB1F","title":"Implement lightweight For You feed algorithm","description":"Add a 'For You' algorithmic feed tab alongside the existing chronological feed. This should be a lightweight scoring system that ranks posts based on:\n\n**Scoring Factors:**\n1. **Engagement score**: likes (weight 3x) + comments (weight 2x) on the post\n2. **Time decay**: Score multiplied by a decay factor based on post age. Use exponential decay with a half-life of ~24 hours so recent engaged content surfaces while popular older content still has a chance.\n3. **Type diversity**: After scoring, apply a diversity pass to avoid showing too many of the same record type in a row. If 3+ consecutive items are the same type, interleave with the next different-type item.\n4. **Social proximity** (future): Boost posts from users the viewer has interacted with (liked their content, commented on their posts). This requires building a per-user interaction graph from the like/comment indexes.\n\n**Implementation approach:**\n- Add a new `FeedSortForYou` sort option alongside recent/popular\n- Add a `scoreForYouItem(item *FeedItem, viewerDID string) float64` function in the firehose index\n- Fetch ~100 recent items, score them, apply diversity, return top N\n- Add a 'For You' tab in the feed filter bar UI (only for authenticated users)\n- Cache scored results per-viewer with short TTL (1-2 min) to avoid re-scoring on pagination\n\n**Key files:**\n- `internal/firehose/index.go` - Add scoring logic and ForYou query\n- `internal/feed/service.go` - Add FeedSortForYou constant\n- `internal/firehose/adapter.go` - Pass through ForYou sort\n- `internal/handlers/feed.go` - Handle sort=foryou param\n- `internal/web/pages/feed.templ` - Add For You tab\n\n**Dependencies:**\n- Relies on existing BucketByTime, BucketLikeCounts, BucketCommentCounts, BucketLikesByActor indexes\n- Social proximity scoring depends on being able to query BucketLikesByActor efficiently","status":"open","priority":"normal","created_at":"2026-02-22T17:34:47.67684351Z","updated_at":"2026-02-22T17:34:47.67684351Z"}
··· 16 {"id":"01KGDXVR86CB2H44DJHKN4Z9M0","title":"Implement comments for social interactions","description":"Add a comment lexicon and implement comment functionality across the app.\n\n## Lexicon: social.arabica.alpha.comment\n\nThe comment record should:\n- Use `com.atproto.repo.strongRef` for the subject (URI + CID for immutability)\n- Support commenting on any arabica.social lexicon type (beans, roasters, grinders, brewers, brews, and other comments)\n- Include text field (max 1000 chars / 300 graphemes as per AT Protocol conventions)\n- Include createdAt timestamp\n- Use TID as record key\n- NOTE: This cell implements flat comments only. Threaded/nested comments are handled in a separate cell.\n\n## Backend Implementation\n\n1. Create lexicon file: `lexicons/social.arabica.alpha.comment.json`\n2. Add NSID constant in `internal/atproto/nsid.go`\n3. Add Comment model in `internal/models/models.go`\n4. Add record conversion functions in `internal/atproto/records.go`\n5. Add Store interface methods: CreateComment, DeleteComment, GetCommentsForSubject, GetUserComments\n6. Implement in AtprotoStore\n7. Update firehose indexing to track comments\n\n## Frontend Implementation\n\n1. Add comment section component below brew detail view\n2. Add comment form (authenticated users only)\n3. Display comment count on records in feed\n4. Update comment counts in response to firehose events\n5. Show commenter profile info (avatar, handle)\n\n## Acceptance Criteria\n\n- Users can comment on any arabica.social record\n- Comments are stored in the user's PDS (actor-owned data)\n- Comment counts display on records in the feed\n- Comments visible on record detail views\n- Real-time comment count updates via firehose","status":"open","priority":"normal","labels":["backend","frontend","atproto"],"created_at":"2026-02-02T01:00:51.846427126Z","updated_at":"2026-02-02T01:00:51.846427126Z"} 17 {"id":"01KGDXW31PHFMBE3WTV83HPET1","title":"Implement comment threading","description":"Add support for threaded/nested comments, building on the flat comment system.\n\n## Lexicon Changes\n\nUpdate `social.arabica.alpha.comment` to add optional threading fields:\n- `parent`: Optional strongRef to parent comment (for replies)\n- `root`: Optional strongRef to root subject (maintains context when replying to comments)\n\nAlternatively, consider a separate reply field or keeping comments flat with UI-level threading.\n\n## Backend Implementation\n\n1. Update comment model to include parent/root references\n2. Add methods to fetch comment threads: GetCommentThread, GetReplies\n3. Update firehose indexing to track parent-child relationships\n4. Add depth limits for threading (prevent infinite nesting)\n\n## Frontend Implementation\n\n1. Design threaded comment UI (indentation, collapse/expand)\n2. Add 'reply' button on comments\n3. Show reply context when replying\n4. Consider max nesting depth for display (e.g., 3-4 levels)\n5. Mobile-friendly thread navigation\n\n## Design Considerations\n\n- How deep should threading go? (Recommend max 3-4 levels visible, then flatten)\n- How to handle deleted parent comments?\n- Should users be notified when someone replies to their comment?\n- Performance: lazy-load deep threads vs eager-load\n\n## Acceptance Criteria\n\n- Users can reply directly to comments\n- Thread structure is visually clear\n- Threads can be collapsed/expanded\n- Works well on mobile devices\n- Parent context shown when replying","status":"blocked","priority":"low","blocked_by":["01KGDXVR86CB2H44DJHKN4Z9M0"],"labels":["frontend","atproto"],"created_at":"2026-02-02T01:01:02.902692829Z","updated_at":"2026-02-02T01:01:06.361891137Z"} 18 {"id":"01KJ36P2BWM4HSZV6GNX0BJB1F","title":"Implement lightweight For You feed algorithm","description":"Add a 'For You' algorithmic feed tab alongside the existing chronological feed. This should be a lightweight scoring system that ranks posts based on:\n\n**Scoring Factors:**\n1. **Engagement score**: likes (weight 3x) + comments (weight 2x) on the post\n2. **Time decay**: Score multiplied by a decay factor based on post age. Use exponential decay with a half-life of ~24 hours so recent engaged content surfaces while popular older content still has a chance.\n3. **Type diversity**: After scoring, apply a diversity pass to avoid showing too many of the same record type in a row. If 3+ consecutive items are the same type, interleave with the next different-type item.\n4. **Social proximity** (future): Boost posts from users the viewer has interacted with (liked their content, commented on their posts). This requires building a per-user interaction graph from the like/comment indexes.\n\n**Implementation approach:**\n- Add a new `FeedSortForYou` sort option alongside recent/popular\n- Add a `scoreForYouItem(item *FeedItem, viewerDID string) float64` function in the firehose index\n- Fetch ~100 recent items, score them, apply diversity, return top N\n- Add a 'For You' tab in the feed filter bar UI (only for authenticated users)\n- Cache scored results per-viewer with short TTL (1-2 min) to avoid re-scoring on pagination\n\n**Key files:**\n- `internal/firehose/index.go` - Add scoring logic and ForYou query\n- `internal/feed/service.go` - Add FeedSortForYou constant\n- `internal/firehose/adapter.go` - Pass through ForYou sort\n- `internal/handlers/feed.go` - Handle sort=foryou param\n- `internal/web/pages/feed.templ` - Add For You tab\n\n**Dependencies:**\n- Relies on existing BucketByTime, BucketLikeCounts, BucketCommentCounts, BucketLikesByActor indexes\n- Social proximity scoring depends on being able to query BucketLikesByActor efficiently","status":"open","priority":"normal","created_at":"2026-02-22T17:34:47.67684351Z","updated_at":"2026-02-22T17:34:47.67684351Z"} 19 + {"id":"01KJ37P3RD4X4P7B8F5EWE6NWB","title":"Add Prometheus metrics endpoint","description":"Add prometheus/client_golang instrumentation to Arabica with a /metrics endpoint. Minimal but useful instrumentation points:\n\n## Metrics to implement\n\n### HTTP middleware (internal/middleware/)\n- `arabica_http_requests_total` counter: labels: method, path, status\n- `arabica_http_request_duration_seconds` histogram: labels: method, path\n\n### Firehose consumer (internal/firehose/)\n- `arabica_firehose_events_total` counter: labels: collection, operation\n- `arabica_firehose_connection_state` gauge: 1=connected, 0=disconnected\n- `arabica_firehose_errors_total` counter\n\n### PDS client (internal/atproto/)\n- `arabica_pds_requests_total` counter: labels: method, collection\n- `arabica_pds_request_duration_seconds` histogram: labels: method\n- `arabica_pds_errors_total` counter: labels: method\n\n### Feed service (internal/feed/)\n- `arabica_feed_cache_hits_total` counter\n- `arabica_feed_cache_misses_total` counter\n\n## Implementation notes\n- Add `github.com/prometheus/client_golang` dependency\n- Expose /metrics endpoint in routing.go (no auth required)\n- Keep cardinality low: normalize path labels (e.g. /brews/{id} -\u003e /brews/:id)\n- Create a grafana/arabica-prometheus.json dashboard alongside the existing log-based one\n- Existing log-based dashboard is at grafana/arabica-logs.json for reference","status":"completed","priority":"normal","assignee":"patrick","labels":["backend","observability"],"created_at":"2026-02-22T17:52:17.677417112Z","updated_at":"2026-02-22T17:57:56.326244513Z","completed_at":"2026-02-22T17:57:56.315753758Z"}
+1
CLAUDE.md
··· 450 | `ARABICA_FEED_INDEX_PATH` | ~/.local/share/arabica/feed-index.db | Firehose index BoltDB path | 451 | `ARABICA_MODERATORS_CONFIG` | - | Path to moderators JSON config | 452 | `ARABICA_PROFILE_CACHE_TTL` | 1h | Profile cache duration | 453 | `SECURE_COOKIES` | false | Set true for HTTPS | 454 | `LOG_LEVEL` | info | debug/info/warn/error | 455 | `LOG_FORMAT` | console | console/json |
··· 450 | `ARABICA_FEED_INDEX_PATH` | ~/.local/share/arabica/feed-index.db | Firehose index BoltDB path | 451 | `ARABICA_MODERATORS_CONFIG` | - | Path to moderators JSON config | 452 | `ARABICA_PROFILE_CACHE_TTL` | 1h | Profile cache duration | 453 + | `METRICS_PORT` | 9101 | Internal metrics server port (localhost only) | 454 | `SECURE_COOKIES` | false | Set true for HTTPS | 455 | `LOG_LEVEL` | info | debug/info/warn/error | 456 | `LOG_FORMAT` | console | console/json |
+45 -1
cmd/server/main.go
··· 20 "arabica/internal/feed" 21 "arabica/internal/firehose" 22 "arabica/internal/handlers" 23 "arabica/internal/moderation" 24 "arabica/internal/routing" 25 26 "github.com/rs/zerolog" 27 "github.com/rs/zerolog/log" 28 ) ··· 193 194 log.Info().Msg("Firehose consumer started") 195 196 // Log known DIDs from database (DIDs discovered via firehose) 197 if knownDIDsFromDB, err := feedIndex.GetKnownDIDs(); err == nil { 198 if len(knownDIDsFromDB) > 0 { ··· 344 Logger: log.Logger, 345 }) 346 347 // Create HTTP server 348 server := &http.Server{ 349 Addr: "0.0.0.0:" + port, ··· 372 log.Info().Msg("Stopping firehose consumer...") 373 firehoseConsumer.Stop() 374 375 - // Graceful shutdown of HTTP server 376 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) 377 defer shutdownCancel() 378 379 if err := server.Shutdown(shutdownCtx); err != nil { 380 log.Error().Err(err).Msg("HTTP server shutdown error")
··· 20 "arabica/internal/feed" 21 "arabica/internal/firehose" 22 "arabica/internal/handlers" 23 + "arabica/internal/metrics" 24 "arabica/internal/moderation" 25 "arabica/internal/routing" 26 27 + "github.com/prometheus/client_golang/prometheus/promhttp" 28 "github.com/rs/zerolog" 29 "github.com/rs/zerolog/log" 30 ) ··· 195 196 log.Info().Msg("Firehose consumer started") 197 198 + // Start metrics collector for periodic gauge updates 199 + metrics.StartCollector(ctx, metrics.StatsSource{ 200 + KnownDIDCount: feedIndex.KnownDIDCount, 201 + RegisteredCount: feedRegistry.Count, 202 + RecordCount: feedIndex.RecordCount, 203 + PendingJoinCount: func() int { 204 + joinStore := store.JoinStore() 205 + if reqs, err := joinStore.ListRequests(); err == nil { 206 + return len(reqs) 207 + } 208 + return 0 209 + }, 210 + LikeCount: feedIndex.TotalLikeCount, 211 + CommentCount: feedIndex.TotalCommentCount, 212 + RecordCountByCollection: feedIndex.RecordCountByCollection, 213 + FirehoseConnected: firehoseConsumer.IsConnected, 214 + }, 60*time.Second) 215 + 216 // Log known DIDs from database (DIDs discovered via firehose) 217 if knownDIDsFromDB, err := feedIndex.GetKnownDIDs(); err == nil { 218 if len(knownDIDsFromDB) > 0 { ··· 364 Logger: log.Logger, 365 }) 366 367 + // Start internal metrics server on localhost only (not publicly accessible) 368 + metricsPort := os.Getenv("METRICS_PORT") 369 + if metricsPort == "" { 370 + metricsPort = "9101" 371 + } 372 + metricsMux := http.NewServeMux() 373 + metricsMux.Handle("GET /metrics", promhttp.Handler()) 374 + metricsServer := &http.Server{ 375 + Addr: "127.0.0.1:" + metricsPort, 376 + Handler: metricsMux, 377 + } 378 + go func() { 379 + log.Info(). 380 + Str("address", "127.0.0.1:"+metricsPort). 381 + Msg("Starting metrics server (localhost only)") 382 + if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { 383 + log.Error().Err(err).Msg("Metrics server failed to start") 384 + } 385 + }() 386 + 387 // Create HTTP server 388 server := &http.Server{ 389 Addr: "0.0.0.0:" + port, ··· 412 log.Info().Msg("Stopping firehose consumer...") 413 firehoseConsumer.Stop() 414 415 + // Graceful shutdown of HTTP server and metrics server 416 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) 417 defer shutdownCancel() 418 + 419 + if err := metricsServer.Shutdown(shutdownCtx); err != nil { 420 + log.Error().Err(err).Msg("Metrics server shutdown error") 421 + } 422 423 if err := server.Shutdown(shutdownCtx); err != nil { 424 log.Error().Err(err).Msg("HTTP server shutdown error")
+1 -1
default.nix
··· 4 pname = "arabica"; 5 version = "0.1.0"; 6 src = ./.; 7 - vendorHash = "sha256-CD6i6qocJ2E5TK7Xprw4bBYmAfZKcy0vMi/krKaMTS8="; 8 9 nativeBuildInputs = [ templ tailwindcss ]; 10
··· 4 pname = "arabica"; 5 version = "0.1.0"; 6 src = ./.; 7 + vendorHash = "sha256-wxCDD46vgdpey8PyeoC2kb7Qd87MN3b+WCsywhpA8RM="; 8 9 nativeBuildInputs = [ templ tailwindcss ]; 10
+10 -9
go.mod
··· 5 require ( 6 github.com/a-h/templ v0.3.977 7 github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725 8 github.com/gorilla/websocket v1.5.3 9 github.com/klauspost/compress v1.18.3 10 github.com/rs/zerolog v1.34.0 11 - github.com/stretchr/testify v1.10.0 12 go.etcd.io/bbolt v1.3.8 13 golang.org/x/sync v0.19.0 14 ) 15 16 require ( 17 github.com/beorn7/perks v1.0.1 // indirect 18 - github.com/cespare/xxhash/v2 v2.2.0 // indirect 19 github.com/davecgh/go-spew v1.1.1 // indirect 20 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 21 github.com/felixge/httpsnoop v1.0.4 // indirect ··· 23 github.com/go-logr/stdr v1.2.2 // indirect 24 github.com/gogo/protobuf v1.3.2 // indirect 25 github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 26 - github.com/google/go-querystring v1.1.0 // indirect 27 github.com/google/uuid v1.4.0 // indirect 28 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 29 github.com/hashicorp/go-retryablehttp v0.7.5 // indirect ··· 45 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 46 github.com/mattn/go-colorable v0.1.13 // indirect 47 github.com/mattn/go-isatty v0.0.20 // indirect 48 - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 49 github.com/minio/sha256-simd v1.0.1 // indirect 50 github.com/mr-tron/base58 v1.2.0 // indirect 51 github.com/multiformats/go-base32 v0.1.0 // indirect ··· 53 github.com/multiformats/go-multibase v0.2.0 // indirect 54 github.com/multiformats/go-multihash v0.2.3 // indirect 55 github.com/multiformats/go-varint v0.0.7 // indirect 56 github.com/opentracing/opentracing-go v1.2.0 // indirect 57 github.com/pmezard/go-difflib v1.0.0 // indirect 58 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 59 - github.com/prometheus/client_golang v1.17.0 // indirect 60 - github.com/prometheus/client_model v0.5.0 // indirect 61 - github.com/prometheus/common v0.45.0 // indirect 62 - github.com/prometheus/procfs v0.12.0 // indirect 63 github.com/spaolacci/murmur3 v1.1.0 // indirect 64 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 65 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect ··· 71 go.uber.org/atomic v1.11.0 // indirect 72 go.uber.org/multierr v1.11.0 // indirect 73 go.uber.org/zap v1.26.0 // indirect 74 golang.org/x/crypto v0.40.0 // indirect 75 golang.org/x/sys v0.36.0 // indirect 76 golang.org/x/time v0.3.0 // indirect 77 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 78 - google.golang.org/protobuf v1.33.0 // indirect 79 gopkg.in/yaml.v3 v3.0.1 // indirect 80 lukechampine.com/blake3 v1.2.1 // indirect 81 )
··· 5 require ( 6 github.com/a-h/templ v0.3.977 7 github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725 8 + github.com/google/go-querystring v1.1.0 9 github.com/gorilla/websocket v1.5.3 10 github.com/klauspost/compress v1.18.3 11 + github.com/prometheus/client_golang v1.23.2 12 + github.com/prometheus/client_model v0.6.2 13 github.com/rs/zerolog v1.34.0 14 + github.com/stretchr/testify v1.11.1 15 go.etcd.io/bbolt v1.3.8 16 golang.org/x/sync v0.19.0 17 ) 18 19 require ( 20 github.com/beorn7/perks v1.0.1 // indirect 21 + github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 github.com/davecgh/go-spew v1.1.1 // indirect 23 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 24 github.com/felixge/httpsnoop v1.0.4 // indirect ··· 26 github.com/go-logr/stdr v1.2.2 // indirect 27 github.com/gogo/protobuf v1.3.2 // indirect 28 github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 29 github.com/google/uuid v1.4.0 // indirect 30 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 31 github.com/hashicorp/go-retryablehttp v0.7.5 // indirect ··· 47 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 48 github.com/mattn/go-colorable v0.1.13 // indirect 49 github.com/mattn/go-isatty v0.0.20 // indirect 50 github.com/minio/sha256-simd v1.0.1 // indirect 51 github.com/mr-tron/base58 v1.2.0 // indirect 52 github.com/multiformats/go-base32 v0.1.0 // indirect ··· 54 github.com/multiformats/go-multibase v0.2.0 // indirect 55 github.com/multiformats/go-multihash v0.2.3 // indirect 56 github.com/multiformats/go-varint v0.0.7 // indirect 57 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 58 github.com/opentracing/opentracing-go v1.2.0 // indirect 59 github.com/pmezard/go-difflib v1.0.0 // indirect 60 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 61 + github.com/prometheus/common v0.66.1 // indirect 62 + github.com/prometheus/procfs v0.16.1 // indirect 63 github.com/spaolacci/murmur3 v1.1.0 // indirect 64 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 65 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect ··· 71 go.uber.org/atomic v1.11.0 // indirect 72 go.uber.org/multierr v1.11.0 // indirect 73 go.uber.org/zap v1.26.0 // indirect 74 + go.yaml.in/yaml/v2 v2.4.2 // indirect 75 golang.org/x/crypto v0.40.0 // indirect 76 golang.org/x/sys v0.36.0 // indirect 77 golang.org/x/time v0.3.0 // indirect 78 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 79 + google.golang.org/protobuf v1.36.8 // indirect 80 gopkg.in/yaml.v3 v3.0.1 // indirect 81 lukechampine.com/blake3 v1.2.1 // indirect 82 )
+24 -20
go.sum
··· 6 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725 h1:gfrLAhE6PHun4MDypO/5hpnaHPd9Dbe9+JxZL0gC4ic= 8 github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725/go.mod h1:KIy0FgNQacp4uv2Z7xhNkV3qZiUSGuRky97s7Pa4v+o= 9 - github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 10 - github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 12 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 13 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= ··· 29 github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 30 github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 31 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 - github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 33 - github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 34 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 35 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 36 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= ··· 95 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 96 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 97 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 98 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 99 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 100 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= ··· 102 github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 103 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 104 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 105 - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 106 - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 107 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 108 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 109 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= ··· 118 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 119 github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 120 github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 121 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 122 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 123 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= ··· 126 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 127 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 128 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 129 - github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 130 - github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 131 - github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 132 - github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 133 - github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 134 - github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 135 - github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 136 - github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 137 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 138 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 139 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= ··· 153 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 154 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 155 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 156 - github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 157 - github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 158 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 159 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 160 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= ··· 182 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 183 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 184 go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 185 - go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 186 - go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 187 go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 188 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 189 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= ··· 193 go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 194 go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 195 go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 196 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 197 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 198 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= ··· 250 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 251 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 252 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 253 - google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 254 - google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 255 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 256 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 257 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
··· 6 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725 h1:gfrLAhE6PHun4MDypO/5hpnaHPd9Dbe9+JxZL0gC4ic= 8 github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725/go.mod h1:KIy0FgNQacp4uv2Z7xhNkV3qZiUSGuRky97s7Pa4v+o= 9 + github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 + github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 12 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 13 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= ··· 29 github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 30 github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 31 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 + github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 33 + github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 34 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 35 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 36 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= ··· 95 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 96 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 97 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 98 + github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 99 + github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 100 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 101 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 102 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= ··· 104 github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 105 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 106 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 107 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 108 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 109 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= ··· 118 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 119 github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 120 github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 121 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 122 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 123 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 124 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 125 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= ··· 128 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 129 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 130 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 131 + github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 132 + github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 133 + github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 134 + github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 135 + github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 136 + github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 137 + github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 138 + github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 139 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 140 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 141 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= ··· 155 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 156 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 157 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 158 + github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 159 + github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 160 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 161 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 162 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= ··· 184 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 185 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 186 go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 187 + go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 188 + go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 189 go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 190 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 191 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= ··· 195 go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 196 go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 197 go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 198 + go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 199 + go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 200 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 201 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 202 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= ··· 254 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 255 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 256 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 257 + google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 258 + google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 259 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 260 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 261 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+50
grafana/README.md
···
··· 1 + # Grafana Dashboards 2 + 3 + Importable Grafana dashboard definitions for monitoring Arabica. 4 + 5 + ## Dashboards 6 + 7 + ### `arabica-logs.json` - Log-Based Metrics 8 + 9 + Queries structured JSON logs via **Loki**. No code changes needed - works with existing zerolog output. 10 + 11 + **Prerequisite:** Ship Arabica logs to Loki (e.g., via Promtail, Alloy, or Docker log driver). Logs must be in JSON format (`LOG_FORMAT=json`). 12 + 13 + **Log selector:** The dashboard uses a template variable (`$log_selector`) with three presets: 14 + 15 + - `unit="arabica.service"` (default) - NixOS/systemd journal via Promtail 16 + - `syslog_identifier="arabica"` - journald syslog identifier 17 + - `app="arabica"` - Docker log driver or custom labels 18 + 19 + Select the matching option from the dropdown at the top of the dashboard, or type a custom value. 20 + 21 + **Sections:** 22 + 23 + - **Overview** - stat panels for total requests, errors, logins, reports, join requests 24 + - **HTTP Traffic** - requests by status/method, top paths, response latency 25 + - **Firehose** - events by collection/operation, errors, backfill activity 26 + - **Authentication & Users** - login success/failure, join requests, invites 27 + - **Moderation** - reports, hide/unhide/block actions, permission denials 28 + - **PDS & ATProto** - PDS request volume/latency/errors by method and collection 29 + - **Errors & Warnings** - error/warn timeline + recent error log viewer 30 + 31 + ### `arabica-prometheus.json` - Prometheus Metrics 32 + 33 + Queries instrumented Prometheus counters, histograms, and gauges exposed at `/metrics`. 34 + 35 + **Prerequisite:** Arabica exposes a `/metrics` endpoint (Prometheus format). Configure Prometheus to scrape it. 36 + 37 + **Sections:** 38 + 39 + - **Overview** - request rate, error rate, p95 latency, firehose connection, events/s, cache hit rate 40 + - **HTTP Traffic** - request rate by status/path, latency percentiles (p50/p95/p99), latency by path 41 + - **Firehose** - events by collection/operation, error rate, connection state 42 + - **PDS / ATProto** - PDS request rate by method/collection, latency by method, error rate 43 + - **Feed Cache** - cache hits vs misses, hit rate over time 44 + 45 + ### Importing 46 + 47 + 1. In Grafana, go to **Dashboards > Import** 48 + 2. Upload the JSON file or paste its contents 49 + 3. Select your data source (Loki or Prometheus) when prompted 50 + 4. For the Loki dashboard, select the correct log selector from the dropdown (defaults to `unit="arabica.service"` for NixOS systemd)
+933
grafana/arabica-logs.json
···
··· 1 + { 2 + "__inputs": [ 3 + { 4 + "name": "DS_LOKI", 5 + "label": "Loki", 6 + "description": "Loki data source for Arabica logs", 7 + "type": "datasource", 8 + "pluginId": "loki", 9 + "pluginName": "Loki" 10 + } 11 + ], 12 + "annotations": { 13 + "list": [ 14 + { 15 + "builtIn": 1, 16 + "datasource": { "type": "grafana", "uid": "-- Grafana --" }, 17 + "enable": true, 18 + "hide": true, 19 + "iconColor": "rgba(0, 211, 255, 1)", 20 + "name": "Annotations & Alerts", 21 + "type": "dashboard" 22 + } 23 + ] 24 + }, 25 + "editable": true, 26 + "fiscalYearStartMonth": 0, 27 + "graphTooltip": 1, 28 + "id": null, 29 + "links": [], 30 + "panels": [ 31 + { 32 + "collapsed": false, 33 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, 34 + "id": 100, 35 + "title": "Overview", 36 + "type": "row" 37 + }, 38 + { 39 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 40 + "fieldConfig": { 41 + "defaults": { 42 + "color": { "mode": "thresholds" }, 43 + "thresholds": { 44 + "steps": [ 45 + { "color": "green", "value": null } 46 + ] 47 + } 48 + }, 49 + "overrides": [] 50 + }, 51 + "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 }, 52 + "id": 1, 53 + "options": { 54 + "colorMode": "background", 55 + "graphMode": "area", 56 + "justifyMode": "auto", 57 + "orientation": "auto", 58 + "reduceOptions": { "calcs": ["count"], "fields": "", "values": false }, 59 + "textMode": "auto" 60 + }, 61 + "title": "Total Requests", 62 + "type": "stat", 63 + "targets": [ 64 + { 65 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 66 + "expr": "count_over_time({${log_selector}} |= `HTTP request` [$__range])", 67 + "refId": "A" 68 + } 69 + ] 70 + }, 71 + { 72 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 73 + "fieldConfig": { 74 + "defaults": { 75 + "color": { "mode": "thresholds" }, 76 + "thresholds": { 77 + "steps": [ 78 + { "color": "green", "value": null }, 79 + { "color": "yellow", "value": 1 }, 80 + { "color": "red", "value": 10 } 81 + ] 82 + } 83 + }, 84 + "overrides": [] 85 + }, 86 + "gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 }, 87 + "id": 2, 88 + "options": { 89 + "colorMode": "background", 90 + "graphMode": "area", 91 + "justifyMode": "auto", 92 + "orientation": "auto", 93 + "reduceOptions": { "calcs": ["count"], "fields": "", "values": false }, 94 + "textMode": "auto" 95 + }, 96 + "title": "5xx Errors", 97 + "type": "stat", 98 + "targets": [ 99 + { 100 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 101 + "expr": "count_over_time({${log_selector}} |= `HTTP request` | json | status >= 500 [$__range])", 102 + "refId": "A" 103 + } 104 + ] 105 + }, 106 + { 107 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 108 + "fieldConfig": { 109 + "defaults": { 110 + "color": { "mode": "thresholds" }, 111 + "thresholds": { 112 + "steps": [ 113 + { "color": "green", "value": null }, 114 + { "color": "yellow", "value": 10 }, 115 + { "color": "orange", "value": 50 } 116 + ] 117 + } 118 + }, 119 + "overrides": [] 120 + }, 121 + "gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 }, 122 + "id": 3, 123 + "options": { 124 + "colorMode": "background", 125 + "graphMode": "area", 126 + "justifyMode": "auto", 127 + "orientation": "auto", 128 + "reduceOptions": { "calcs": ["count"], "fields": "", "values": false }, 129 + "textMode": "auto" 130 + }, 131 + "title": "4xx Errors", 132 + "type": "stat", 133 + "targets": [ 134 + { 135 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 136 + "expr": "count_over_time({${log_selector}} |= `HTTP request` | json | status >= 400 | status < 500 [$__range])", 137 + "refId": "A" 138 + } 139 + ] 140 + }, 141 + { 142 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 143 + "fieldConfig": { 144 + "defaults": { 145 + "color": { "mode": "thresholds" }, 146 + "thresholds": { 147 + "steps": [ 148 + { "color": "blue", "value": null } 149 + ] 150 + } 151 + }, 152 + "overrides": [] 153 + }, 154 + "gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 }, 155 + "id": 4, 156 + "options": { 157 + "colorMode": "background", 158 + "graphMode": "none", 159 + "justifyMode": "auto", 160 + "orientation": "auto", 161 + "reduceOptions": { "calcs": ["count"], "fields": "", "values": false }, 162 + "textMode": "auto" 163 + }, 164 + "title": "Unique Users", 165 + "type": "stat", 166 + "targets": [ 167 + { 168 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 169 + "expr": "count_over_time({${log_selector}} |= `User logged in successfully` [$__range])", 170 + "refId": "A" 171 + } 172 + ] 173 + }, 174 + { 175 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 176 + "fieldConfig": { 177 + "defaults": { 178 + "color": { "mode": "thresholds" }, 179 + "thresholds": { 180 + "steps": [ 181 + { "color": "purple", "value": null } 182 + ] 183 + } 184 + }, 185 + "overrides": [] 186 + }, 187 + "gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 }, 188 + "id": 5, 189 + "options": { 190 + "colorMode": "background", 191 + "graphMode": "area", 192 + "justifyMode": "auto", 193 + "orientation": "auto", 194 + "reduceOptions": { "calcs": ["count"], "fields": "", "values": false }, 195 + "textMode": "auto" 196 + }, 197 + "title": "Reports Filed", 198 + "type": "stat", 199 + "targets": [ 200 + { 201 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 202 + "expr": "count_over_time({${log_selector}} |= `Report created successfully` [$__range])", 203 + "refId": "A" 204 + } 205 + ] 206 + }, 207 + { 208 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 209 + "fieldConfig": { 210 + "defaults": { 211 + "color": { "mode": "thresholds" }, 212 + "thresholds": { 213 + "steps": [ 214 + { "color": "orange", "value": null } 215 + ] 216 + } 217 + }, 218 + "overrides": [] 219 + }, 220 + "gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 }, 221 + "id": 6, 222 + "options": { 223 + "colorMode": "background", 224 + "graphMode": "area", 225 + "justifyMode": "auto", 226 + "orientation": "auto", 227 + "reduceOptions": { "calcs": ["count"], "fields": "", "values": false }, 228 + "textMode": "auto" 229 + }, 230 + "title": "Join Requests", 231 + "type": "stat", 232 + "targets": [ 233 + { 234 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 235 + "expr": "count_over_time({${log_selector}} |= `Join request saved` [$__range])", 236 + "refId": "A" 237 + } 238 + ] 239 + }, 240 + { 241 + "collapsed": false, 242 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, 243 + "id": 101, 244 + "title": "HTTP Traffic", 245 + "type": "row" 246 + }, 247 + { 248 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 249 + "fieldConfig": { 250 + "defaults": { 251 + "color": { "mode": "palette-classic" }, 252 + "custom": { 253 + "axisBorderShow": false, 254 + "drawStyle": "bars", 255 + "fillOpacity": 80, 256 + "stacking": { "mode": "normal" }, 257 + "lineWidth": 0, 258 + "pointSize": 5 259 + } 260 + }, 261 + "overrides": [] 262 + }, 263 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 }, 264 + "id": 10, 265 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 266 + "title": "Requests by Status", 267 + "type": "timeseries", 268 + "targets": [ 269 + { 270 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 271 + "expr": "sum by (status) (count_over_time({${log_selector}} |= `HTTP request` | json | status >= 200 | status < 300 [$__auto]))", 272 + "legendFormat": "2xx", 273 + "refId": "A" 274 + }, 275 + { 276 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 277 + "expr": "sum by (status) (count_over_time({${log_selector}} |= `HTTP request` | json | status >= 300 | status < 400 [$__auto]))", 278 + "legendFormat": "3xx", 279 + "refId": "B" 280 + }, 281 + { 282 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 283 + "expr": "sum by (status) (count_over_time({${log_selector}} |= `HTTP request` | json | status >= 400 | status < 500 [$__auto]))", 284 + "legendFormat": "4xx", 285 + "refId": "C" 286 + }, 287 + { 288 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 289 + "expr": "sum by (status) (count_over_time({${log_selector}} |= `HTTP request` | json | status >= 500 [$__auto]))", 290 + "legendFormat": "5xx", 291 + "refId": "D" 292 + } 293 + ] 294 + }, 295 + { 296 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 297 + "fieldConfig": { 298 + "defaults": { 299 + "color": { "mode": "palette-classic" }, 300 + "custom": { 301 + "axisBorderShow": false, 302 + "drawStyle": "bars", 303 + "fillOpacity": 60, 304 + "stacking": { "mode": "normal" }, 305 + "lineWidth": 0, 306 + "pointSize": 5 307 + } 308 + }, 309 + "overrides": [] 310 + }, 311 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 }, 312 + "id": 11, 313 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 314 + "title": "Requests by Method", 315 + "type": "timeseries", 316 + "targets": [ 317 + { 318 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 319 + "expr": "sum by (method) (count_over_time({${log_selector}} |= `HTTP request` | json [$__auto]))", 320 + "legendFormat": "{{method}}", 321 + "refId": "A" 322 + } 323 + ] 324 + }, 325 + { 326 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 327 + "fieldConfig": { 328 + "defaults": { 329 + "color": { "mode": "palette-classic" }, 330 + "custom": { 331 + "axisBorderShow": false, 332 + "drawStyle": "bars", 333 + "fillOpacity": 60, 334 + "stacking": { "mode": "normal" }, 335 + "lineWidth": 0, 336 + "pointSize": 5 337 + } 338 + }, 339 + "overrides": [] 340 + }, 341 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 14 }, 342 + "id": 12, 343 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 344 + "title": "Top Paths", 345 + "type": "timeseries", 346 + "targets": [ 347 + { 348 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 349 + "expr": "sum by (path) (count_over_time({${log_selector}} |= `HTTP request` | json | path !~ `/static/.*` [$__auto])) ", 350 + "legendFormat": "{{path}}", 351 + "refId": "A" 352 + } 353 + ] 354 + }, 355 + { 356 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 357 + "fieldConfig": { 358 + "defaults": { 359 + "color": { "mode": "palette-classic" }, 360 + "custom": { 361 + "axisBorderShow": false, 362 + "drawStyle": "line", 363 + "fillOpacity": 20, 364 + "lineWidth": 2, 365 + "pointSize": 5 366 + }, 367 + "unit": "ms" 368 + }, 369 + "overrides": [] 370 + }, 371 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 14 }, 372 + "id": 13, 373 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 374 + "title": "Response Time (avg per interval)", 375 + "type": "timeseries", 376 + "targets": [ 377 + { 378 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 379 + "expr": "avg_over_time({${log_selector}} |= `HTTP request` | json | path !~ `/static/.*` | unwrap duration [$__auto]) / 1000000", 380 + "legendFormat": "avg latency", 381 + "refId": "A" 382 + } 383 + ] 384 + }, 385 + { 386 + "collapsed": false, 387 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 }, 388 + "id": 102, 389 + "title": "Firehose", 390 + "type": "row" 391 + }, 392 + { 393 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 394 + "fieldConfig": { 395 + "defaults": { 396 + "color": { "mode": "palette-classic" }, 397 + "custom": { 398 + "axisBorderShow": false, 399 + "drawStyle": "bars", 400 + "fillOpacity": 60, 401 + "stacking": { "mode": "normal" }, 402 + "lineWidth": 0, 403 + "pointSize": 5 404 + } 405 + }, 406 + "overrides": [] 407 + }, 408 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 23 }, 409 + "id": 20, 410 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 411 + "title": "Firehose Events by Collection", 412 + "type": "timeseries", 413 + "targets": [ 414 + { 415 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 416 + "expr": "sum by (collection) (count_over_time({${log_selector}} |= `firehose: processing event` | json [$__auto]))", 417 + "legendFormat": "{{collection}}", 418 + "refId": "A" 419 + } 420 + ] 421 + }, 422 + { 423 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 424 + "fieldConfig": { 425 + "defaults": { 426 + "color": { "mode": "palette-classic" }, 427 + "custom": { 428 + "axisBorderShow": false, 429 + "drawStyle": "bars", 430 + "fillOpacity": 60, 431 + "stacking": { "mode": "normal" }, 432 + "lineWidth": 0, 433 + "pointSize": 5 434 + } 435 + }, 436 + "overrides": [] 437 + }, 438 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 23 }, 439 + "id": 21, 440 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 441 + "title": "Firehose Events by Operation", 442 + "type": "timeseries", 443 + "targets": [ 444 + { 445 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 446 + "expr": "sum by (operation) (count_over_time({${log_selector}} |= `firehose: processing event` | json [$__auto]))", 447 + "legendFormat": "{{operation}}", 448 + "refId": "A" 449 + } 450 + ] 451 + }, 452 + { 453 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 454 + "fieldConfig": { 455 + "defaults": { 456 + "color": { "mode": "thresholds" }, 457 + "thresholds": { 458 + "steps": [ 459 + { "color": "green", "value": null }, 460 + { "color": "yellow", "value": 1 }, 461 + { "color": "red", "value": 5 } 462 + ] 463 + } 464 + }, 465 + "overrides": [] 466 + }, 467 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 31 }, 468 + "id": 22, 469 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 470 + "title": "Firehose Errors", 471 + "type": "timeseries", 472 + "targets": [ 473 + { 474 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 475 + "expr": "count_over_time({${log_selector}} |= `firehose:` |~ `\"level\":\"(warn|error)\"` [$__auto])", 476 + "legendFormat": "errors", 477 + "refId": "A" 478 + } 479 + ] 480 + }, 481 + { 482 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 483 + "fieldConfig": { 484 + "defaults": { 485 + "color": { "mode": "palette-classic" }, 486 + "custom": { 487 + "axisBorderShow": false, 488 + "drawStyle": "bars", 489 + "fillOpacity": 60, 490 + "lineWidth": 0, 491 + "pointSize": 5 492 + } 493 + }, 494 + "overrides": [] 495 + }, 496 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 31 }, 497 + "id": 23, 498 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 499 + "title": "Backfills", 500 + "type": "timeseries", 501 + "targets": [ 502 + { 503 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 504 + "expr": "count_over_time({${log_selector}} |= `backfill complete` [$__auto])", 505 + "legendFormat": "completed", 506 + "refId": "A" 507 + }, 508 + { 509 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 510 + "expr": "count_over_time({${log_selector}} |= `backfilling user records` [$__auto])", 511 + "legendFormat": "started", 512 + "refId": "B" 513 + } 514 + ] 515 + }, 516 + { 517 + "collapsed": false, 518 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 39 }, 519 + "id": 103, 520 + "title": "Authentication & Users", 521 + "type": "row" 522 + }, 523 + { 524 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 525 + "fieldConfig": { 526 + "defaults": { 527 + "color": { "mode": "palette-classic" }, 528 + "custom": { 529 + "axisBorderShow": false, 530 + "drawStyle": "bars", 531 + "fillOpacity": 60, 532 + "lineWidth": 0, 533 + "pointSize": 5 534 + } 535 + }, 536 + "overrides": [] 537 + }, 538 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 40 }, 539 + "id": 30, 540 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 541 + "title": "Logins", 542 + "type": "timeseries", 543 + "targets": [ 544 + { 545 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 546 + "expr": "count_over_time({${log_selector}} |= `User logged in successfully` [$__auto])", 547 + "legendFormat": "successful logins", 548 + "refId": "A" 549 + }, 550 + { 551 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 552 + "expr": "count_over_time({${log_selector}} |= `Failed to initiate login` [$__auto])", 553 + "legendFormat": "failed logins", 554 + "refId": "B" 555 + }, 556 + { 557 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 558 + "expr": "count_over_time({${log_selector}} |= `Failed to complete OAuth flow` [$__auto])", 559 + "legendFormat": "failed OAuth callbacks", 560 + "refId": "C" 561 + } 562 + ] 563 + }, 564 + { 565 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 566 + "fieldConfig": { 567 + "defaults": { 568 + "color": { "mode": "palette-classic" }, 569 + "custom": { 570 + "axisBorderShow": false, 571 + "drawStyle": "bars", 572 + "fillOpacity": 60, 573 + "lineWidth": 0, 574 + "pointSize": 5 575 + } 576 + }, 577 + "overrides": [] 578 + }, 579 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 40 }, 580 + "id": 31, 581 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 582 + "title": "Join Requests & Invites", 583 + "type": "timeseries", 584 + "targets": [ 585 + { 586 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 587 + "expr": "count_over_time({${log_selector}} |= `Join request saved` [$__auto])", 588 + "legendFormat": "join requests", 589 + "refId": "A" 590 + }, 591 + { 592 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 593 + "expr": "count_over_time({${log_selector}} |= `Invite code created` [$__auto])", 594 + "legendFormat": "invites created", 595 + "refId": "B" 596 + }, 597 + { 598 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 599 + "expr": "count_over_time({${log_selector}} |= `Account created` [$__auto])", 600 + "legendFormat": "accounts created", 601 + "refId": "C" 602 + } 603 + ] 604 + }, 605 + { 606 + "collapsed": false, 607 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 48 }, 608 + "id": 104, 609 + "title": "Moderation", 610 + "type": "row" 611 + }, 612 + { 613 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 614 + "fieldConfig": { 615 + "defaults": { 616 + "color": { "mode": "palette-classic" }, 617 + "custom": { 618 + "axisBorderShow": false, 619 + "drawStyle": "bars", 620 + "fillOpacity": 60, 621 + "lineWidth": 0, 622 + "pointSize": 5 623 + } 624 + }, 625 + "overrides": [] 626 + }, 627 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 49 }, 628 + "id": 40, 629 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 630 + "title": "Moderation Actions", 631 + "type": "timeseries", 632 + "targets": [ 633 + { 634 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 635 + "expr": "count_over_time({${log_selector}} |= `Report created successfully` [$__auto])", 636 + "legendFormat": "reports", 637 + "refId": "A" 638 + }, 639 + { 640 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 641 + "expr": "count_over_time({${log_selector}} |= `Record hidden successfully` [$__auto])", 642 + "legendFormat": "records hidden", 643 + "refId": "B" 644 + }, 645 + { 646 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 647 + "expr": "count_over_time({${log_selector}} |= `Record unhidden successfully` [$__auto])", 648 + "legendFormat": "records unhidden", 649 + "refId": "C" 650 + }, 651 + { 652 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 653 + "expr": "count_over_time({${log_selector}} |= `User blocked successfully` [$__auto])", 654 + "legendFormat": "users blocked", 655 + "refId": "D" 656 + } 657 + ] 658 + }, 659 + { 660 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 661 + "fieldConfig": { 662 + "defaults": { 663 + "color": { "mode": "palette-classic" }, 664 + "custom": { 665 + "axisBorderShow": false, 666 + "drawStyle": "bars", 667 + "fillOpacity": 60, 668 + "lineWidth": 0, 669 + "pointSize": 5 670 + } 671 + }, 672 + "overrides": [] 673 + }, 674 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 49 }, 675 + "id": 41, 676 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 677 + "title": "Permission Denials", 678 + "type": "timeseries", 679 + "targets": [ 680 + { 681 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 682 + "expr": "count_over_time({${log_selector}} |= `Denied:` [$__auto])", 683 + "legendFormat": "denied", 684 + "refId": "A" 685 + } 686 + ] 687 + }, 688 + { 689 + "collapsed": false, 690 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 57 }, 691 + "id": 105, 692 + "title": "PDS & ATProto", 693 + "type": "row" 694 + }, 695 + { 696 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 697 + "fieldConfig": { 698 + "defaults": { 699 + "color": { "mode": "palette-classic" }, 700 + "custom": { 701 + "axisBorderShow": false, 702 + "drawStyle": "bars", 703 + "fillOpacity": 60, 704 + "stacking": { "mode": "normal" }, 705 + "lineWidth": 0, 706 + "pointSize": 5 707 + } 708 + }, 709 + "overrides": [] 710 + }, 711 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 58 }, 712 + "id": 50, 713 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 714 + "title": "PDS Requests by Method", 715 + "type": "timeseries", 716 + "targets": [ 717 + { 718 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 719 + "expr": "sum by (method) (count_over_time({${log_selector}} |= `PDS request completed` | json [$__auto]))", 720 + "legendFormat": "{{method}}", 721 + "refId": "A" 722 + } 723 + ] 724 + }, 725 + { 726 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 727 + "fieldConfig": { 728 + "defaults": { 729 + "color": { "mode": "palette-classic" }, 730 + "custom": { 731 + "axisBorderShow": false, 732 + "drawStyle": "line", 733 + "fillOpacity": 20, 734 + "lineWidth": 2, 735 + "pointSize": 5 736 + }, 737 + "unit": "ms" 738 + }, 739 + "overrides": [] 740 + }, 741 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 58 }, 742 + "id": 51, 743 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 744 + "title": "PDS Latency", 745 + "type": "timeseries", 746 + "targets": [ 747 + { 748 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 749 + "expr": "avg_over_time({${log_selector}} |= `PDS request completed` | json | unwrap duration [$__auto]) / 1000000", 750 + "legendFormat": "avg", 751 + "refId": "A" 752 + } 753 + ] 754 + }, 755 + { 756 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 757 + "fieldConfig": { 758 + "defaults": { 759 + "color": { "mode": "thresholds" }, 760 + "thresholds": { 761 + "steps": [ 762 + { "color": "green", "value": null }, 763 + { "color": "red", "value": 1 } 764 + ] 765 + } 766 + }, 767 + "overrides": [] 768 + }, 769 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 66 }, 770 + "id": 52, 771 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 772 + "title": "PDS Errors", 773 + "type": "timeseries", 774 + "targets": [ 775 + { 776 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 777 + "expr": "count_over_time({${log_selector}} |= `PDS request failed` [$__auto])", 778 + "legendFormat": "PDS failures", 779 + "refId": "A" 780 + } 781 + ] 782 + }, 783 + { 784 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 785 + "fieldConfig": { 786 + "defaults": { 787 + "color": { "mode": "palette-classic" }, 788 + "custom": { 789 + "axisBorderShow": false, 790 + "drawStyle": "bars", 791 + "fillOpacity": 60, 792 + "stacking": { "mode": "normal" }, 793 + "lineWidth": 0, 794 + "pointSize": 5 795 + } 796 + }, 797 + "overrides": [] 798 + }, 799 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 66 }, 800 + "id": 53, 801 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 802 + "title": "PDS Requests by Collection", 803 + "type": "timeseries", 804 + "targets": [ 805 + { 806 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 807 + "expr": "sum by (collection) (count_over_time({${log_selector}} |= `PDS request completed` | json [$__auto]))", 808 + "legendFormat": "{{collection}}", 809 + "refId": "A" 810 + } 811 + ] 812 + }, 813 + { 814 + "collapsed": false, 815 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 74 }, 816 + "id": 106, 817 + "title": "Errors & Warnings", 818 + "type": "row" 819 + }, 820 + { 821 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 822 + "fieldConfig": { 823 + "defaults": { 824 + "color": { "mode": "palette-classic" }, 825 + "custom": { 826 + "axisBorderShow": false, 827 + "drawStyle": "bars", 828 + "fillOpacity": 80, 829 + "stacking": { "mode": "normal" }, 830 + "lineWidth": 0, 831 + "pointSize": 5 832 + } 833 + }, 834 + "overrides": [ 835 + { 836 + "matcher": { "id": "byName", "options": "error" }, 837 + "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] 838 + }, 839 + { 840 + "matcher": { "id": "byName", "options": "warn" }, 841 + "properties": [{ "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } }] 842 + } 843 + ] 844 + }, 845 + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 75 }, 846 + "id": 60, 847 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 848 + "title": "Errors & Warnings Over Time", 849 + "type": "timeseries", 850 + "targets": [ 851 + { 852 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 853 + "expr": "count_over_time({${log_selector}} |~ `\"level\":\"error\"` [$__auto])", 854 + "legendFormat": "error", 855 + "refId": "A" 856 + }, 857 + { 858 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 859 + "expr": "count_over_time({${log_selector}} |~ `\"level\":\"warn\"` [$__auto])", 860 + "legendFormat": "warn", 861 + "refId": "B" 862 + } 863 + ] 864 + }, 865 + { 866 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 867 + "gridPos": { "h": 12, "w": 24, "x": 0, "y": 83 }, 868 + "id": 61, 869 + "options": { 870 + "dedupStrategy": "none", 871 + "enableLogDetails": true, 872 + "prettifyLogMessage": true, 873 + "showCommonLabels": false, 874 + "showLabels": false, 875 + "showTime": true, 876 + "sortOrder": "Descending", 877 + "wrapLogMessage": true 878 + }, 879 + "title": "Recent Errors", 880 + "type": "logs", 881 + "targets": [ 882 + { 883 + "datasource": { "type": "loki", "uid": "${DS_LOKI}" }, 884 + "expr": "{${log_selector}} |~ `\"level\":\"error\"`", 885 + "refId": "A" 886 + } 887 + ] 888 + } 889 + ], 890 + "schemaVersion": 39, 891 + "tags": ["arabica", "loki", "logs"], 892 + "templating": { 893 + "list": [ 894 + { 895 + "current": { 896 + "selected": false, 897 + "text": "unit=\"arabica.service\"", 898 + "value": "unit=\"arabica.service\"" 899 + }, 900 + "description": "Loki stream selector for Arabica logs. Common values: unit=\"arabica.service\" (journald via Promtail), syslog_identifier=\"arabica\" (journald syslog), app=\"arabica\" (Docker/custom labels)", 901 + "hide": 0, 902 + "label": "Log Selector", 903 + "name": "log_selector", 904 + "options": [ 905 + { 906 + "selected": true, 907 + "text": "unit=\"arabica.service\"", 908 + "value": "unit=\"arabica.service\"" 909 + }, 910 + { 911 + "selected": false, 912 + "text": "syslog_identifier=\"arabica\"", 913 + "value": "syslog_identifier=\"arabica\"" 914 + }, 915 + { 916 + "selected": false, 917 + "text": "app=\"arabica\"", 918 + "value": "app=\"arabica\"" 919 + } 920 + ], 921 + "query": "unit=\"arabica.service\",syslog_identifier=\"arabica\",app=\"arabica\"", 922 + "skipUrlSync": false, 923 + "type": "custom" 924 + } 925 + ] 926 + }, 927 + "time": { "from": "now-24h", "to": "now" }, 928 + "timepicker": {}, 929 + "timezone": "", 930 + "title": "Arabica - Log Metrics", 931 + "uid": "arabica-logs", 932 + "version": 1 933 + }
+806
grafana/arabica-prometheus.json
···
··· 1 + { 2 + "__inputs": [ 3 + { 4 + "name": "DS_PROMETHEUS", 5 + "label": "Prometheus", 6 + "description": "Prometheus data source for Arabica metrics", 7 + "type": "datasource", 8 + "pluginId": "prometheus", 9 + "pluginName": "Prometheus" 10 + } 11 + ], 12 + "annotations": { 13 + "list": [ 14 + { 15 + "builtIn": 1, 16 + "datasource": { "type": "grafana", "uid": "-- Grafana --" }, 17 + "enable": true, 18 + "hide": true, 19 + "iconColor": "rgba(0, 211, 255, 1)", 20 + "name": "Annotations & Alerts", 21 + "type": "dashboard" 22 + } 23 + ] 24 + }, 25 + "editable": true, 26 + "fiscalYearStartMonth": 0, 27 + "graphTooltip": 1, 28 + "id": null, 29 + "links": [], 30 + "panels": [ 31 + { 32 + "collapsed": false, 33 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, 34 + "id": 100, 35 + "title": "Overview", 36 + "type": "row" 37 + }, 38 + { 39 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 40 + "fieldConfig": { 41 + "defaults": { 42 + "color": { "mode": "thresholds" }, 43 + "thresholds": { "steps": [{ "color": "green", "value": null }] } 44 + } 45 + }, 46 + "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 }, 47 + "id": 1, 48 + "options": { "colorMode": "background", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, 49 + "title": "Request Rate", 50 + "type": "stat", 51 + "targets": [ 52 + { 53 + "expr": "sum(rate(arabica_http_requests_total[5m]))", 54 + "legendFormat": "req/s", 55 + "refId": "A" 56 + } 57 + ] 58 + }, 59 + { 60 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 61 + "fieldConfig": { 62 + "defaults": { 63 + "color": { "mode": "thresholds" }, 64 + "thresholds": { "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 0.01 }] }, 65 + "unit": "percentunit" 66 + } 67 + }, 68 + "gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 }, 69 + "id": 2, 70 + "options": { "colorMode": "background", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, 71 + "title": "Error Rate (5xx)", 72 + "type": "stat", 73 + "targets": [ 74 + { 75 + "expr": "sum(rate(arabica_http_requests_total{status=~\"5..\"}[5m])) / sum(rate(arabica_http_requests_total[5m]))", 76 + "legendFormat": "", 77 + "refId": "A" 78 + } 79 + ] 80 + }, 81 + { 82 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 83 + "fieldConfig": { 84 + "defaults": { 85 + "color": { "mode": "thresholds" }, 86 + "thresholds": { "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 0.25 }, { "color": "red", "value": 1 }] }, 87 + "unit": "s" 88 + } 89 + }, 90 + "gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 }, 91 + "id": 3, 92 + "options": { "colorMode": "background", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, 93 + "title": "p95 Latency", 94 + "type": "stat", 95 + "targets": [ 96 + { 97 + "expr": "histogram_quantile(0.95, sum(rate(arabica_http_request_duration_seconds_bucket[5m])) by (le))", 98 + "legendFormat": "", 99 + "refId": "A" 100 + } 101 + ] 102 + }, 103 + { 104 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 105 + "fieldConfig": { 106 + "defaults": { 107 + "color": { "mode": "thresholds" }, 108 + "thresholds": { "steps": [{ "color": "blue", "value": null }] } 109 + } 110 + }, 111 + "gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 }, 112 + "id": 4, 113 + "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }, 114 + "title": "Firehose Connected", 115 + "type": "stat", 116 + "targets": [ 117 + { 118 + "expr": "arabica_firehose_connection_state", 119 + "legendFormat": "", 120 + "refId": "A" 121 + } 122 + ] 123 + }, 124 + { 125 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 126 + "fieldConfig": { 127 + "defaults": { 128 + "color": { "mode": "thresholds" }, 129 + "thresholds": { "steps": [{ "color": "purple", "value": null }] } 130 + } 131 + }, 132 + "gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 }, 133 + "id": 5, 134 + "options": { "colorMode": "background", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, 135 + "title": "Firehose Events/s", 136 + "type": "stat", 137 + "targets": [ 138 + { 139 + "expr": "sum(rate(arabica_firehose_events_total[5m]))", 140 + "legendFormat": "", 141 + "refId": "A" 142 + } 143 + ] 144 + }, 145 + { 146 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 147 + "fieldConfig": { 148 + "defaults": { 149 + "color": { "mode": "thresholds" }, 150 + "thresholds": { "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 0.5 }] }, 151 + "unit": "percentunit" 152 + } 153 + }, 154 + "gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 }, 155 + "id": 6, 156 + "options": { "colorMode": "background", "graphMode": "area", "reduceOptions": { "calcs": ["lastNotNull"] } }, 157 + "title": "Feed Cache Hit Rate", 158 + "type": "stat", 159 + "targets": [ 160 + { 161 + "expr": "rate(arabica_feed_cache_hits_total[5m]) / (rate(arabica_feed_cache_hits_total[5m]) + rate(arabica_feed_cache_misses_total[5m]))", 162 + "legendFormat": "", 163 + "refId": "A" 164 + } 165 + ] 166 + }, 167 + { 168 + "collapsed": false, 169 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, 170 + "id": 101, 171 + "title": "HTTP Traffic", 172 + "type": "row" 173 + }, 174 + { 175 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 176 + "fieldConfig": { 177 + "defaults": { 178 + "color": { "mode": "palette-classic" }, 179 + "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2, "stacking": { "mode": "normal" } } 180 + } 181 + }, 182 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 }, 183 + "id": 10, 184 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 185 + "title": "Request Rate by Status", 186 + "type": "timeseries", 187 + "targets": [ 188 + { 189 + "expr": "sum by (status) (rate(arabica_http_requests_total{status=~\"2..\"}[5m]))", 190 + "legendFormat": "2xx", 191 + "refId": "A" 192 + }, 193 + { 194 + "expr": "sum by (status) (rate(arabica_http_requests_total{status=~\"3..\"}[5m]))", 195 + "legendFormat": "3xx", 196 + "refId": "B" 197 + }, 198 + { 199 + "expr": "sum by (status) (rate(arabica_http_requests_total{status=~\"4..\"}[5m]))", 200 + "legendFormat": "4xx", 201 + "refId": "C" 202 + }, 203 + { 204 + "expr": "sum by (status) (rate(arabica_http_requests_total{status=~\"5..\"}[5m]))", 205 + "legendFormat": "5xx", 206 + "refId": "D" 207 + } 208 + ] 209 + }, 210 + { 211 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 212 + "fieldConfig": { 213 + "defaults": { 214 + "color": { "mode": "palette-classic" }, 215 + "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2, "stacking": { "mode": "normal" } } 216 + } 217 + }, 218 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 }, 219 + "id": 11, 220 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 221 + "title": "Request Rate by Path", 222 + "type": "timeseries", 223 + "targets": [ 224 + { 225 + "expr": "sum by (path) (rate(arabica_http_requests_total{path!=\"/static/*\"}[5m])) > 0", 226 + "legendFormat": "{{path}}", 227 + "refId": "A" 228 + } 229 + ] 230 + }, 231 + { 232 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 233 + "fieldConfig": { 234 + "defaults": { 235 + "color": { "mode": "palette-classic" }, 236 + "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 }, 237 + "unit": "s" 238 + } 239 + }, 240 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 14 }, 241 + "id": 12, 242 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 243 + "title": "Request Latency Percentiles", 244 + "type": "timeseries", 245 + "targets": [ 246 + { 247 + "expr": "histogram_quantile(0.50, sum(rate(arabica_http_request_duration_seconds_bucket{path!=\"/static/*\"}[5m])) by (le))", 248 + "legendFormat": "p50", 249 + "refId": "A" 250 + }, 251 + { 252 + "expr": "histogram_quantile(0.95, sum(rate(arabica_http_request_duration_seconds_bucket{path!=\"/static/*\"}[5m])) by (le))", 253 + "legendFormat": "p95", 254 + "refId": "B" 255 + }, 256 + { 257 + "expr": "histogram_quantile(0.99, sum(rate(arabica_http_request_duration_seconds_bucket{path!=\"/static/*\"}[5m])) by (le))", 258 + "legendFormat": "p99", 259 + "refId": "C" 260 + } 261 + ] 262 + }, 263 + { 264 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 265 + "fieldConfig": { 266 + "defaults": { 267 + "color": { "mode": "palette-classic" }, 268 + "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 }, 269 + "unit": "s" 270 + } 271 + }, 272 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 14 }, 273 + "id": 13, 274 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 275 + "title": "Latency by Path (p95)", 276 + "type": "timeseries", 277 + "targets": [ 278 + { 279 + "expr": "histogram_quantile(0.95, sum by (le, path) (rate(arabica_http_request_duration_seconds_bucket{path!=\"/static/*\"}[5m])))", 280 + "legendFormat": "{{path}}", 281 + "refId": "A" 282 + } 283 + ] 284 + }, 285 + { 286 + "collapsed": false, 287 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 }, 288 + "id": 102, 289 + "title": "Firehose", 290 + "type": "row" 291 + }, 292 + { 293 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 294 + "fieldConfig": { 295 + "defaults": { 296 + "color": { "mode": "palette-classic" }, 297 + "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2, "stacking": { "mode": "normal" } } 298 + } 299 + }, 300 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 23 }, 301 + "id": 20, 302 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 303 + "title": "Events by Collection", 304 + "type": "timeseries", 305 + "targets": [ 306 + { 307 + "expr": "sum by (collection) (rate(arabica_firehose_events_total[5m]))", 308 + "legendFormat": "{{collection}}", 309 + "refId": "A" 310 + } 311 + ] 312 + }, 313 + { 314 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 315 + "fieldConfig": { 316 + "defaults": { 317 + "color": { "mode": "palette-classic" }, 318 + "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2, "stacking": { "mode": "normal" } } 319 + } 320 + }, 321 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 23 }, 322 + "id": 21, 323 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 324 + "title": "Events by Operation", 325 + "type": "timeseries", 326 + "targets": [ 327 + { 328 + "expr": "sum by (operation) (rate(arabica_firehose_events_total[5m]))", 329 + "legendFormat": "{{operation}}", 330 + "refId": "A" 331 + } 332 + ] 333 + }, 334 + { 335 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 336 + "fieldConfig": { 337 + "defaults": { 338 + "color": { "mode": "thresholds" }, 339 + "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 }, 340 + "thresholds": { "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 0.1 }] } 341 + } 342 + }, 343 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 31 }, 344 + "id": 22, 345 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 346 + "title": "Firehose Error Rate", 347 + "type": "timeseries", 348 + "targets": [ 349 + { 350 + "expr": "rate(arabica_firehose_errors_total[5m])", 351 + "legendFormat": "errors/s", 352 + "refId": "A" 353 + } 354 + ] 355 + }, 356 + { 357 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 358 + "fieldConfig": { 359 + "defaults": { 360 + "color": { "mode": "thresholds" }, 361 + "mappings": [ 362 + { "options": { "0": { "color": "red", "text": "Disconnected" }, "1": { "color": "green", "text": "Connected" } }, "type": "value" } 363 + ], 364 + "thresholds": { "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] } 365 + } 366 + }, 367 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 31 }, 368 + "id": 23, 369 + "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }, 370 + "title": "Connection State", 371 + "type": "stat" , 372 + "targets": [ 373 + { 374 + "expr": "arabica_firehose_connection_state", 375 + "legendFormat": "", 376 + "refId": "A" 377 + } 378 + ] 379 + }, 380 + { 381 + "collapsed": false, 382 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 39 }, 383 + "id": 103, 384 + "title": "PDS / ATProto", 385 + "type": "row" 386 + }, 387 + { 388 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 389 + "fieldConfig": { 390 + "defaults": { 391 + "color": { "mode": "palette-classic" }, 392 + "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2, "stacking": { "mode": "normal" } } 393 + } 394 + }, 395 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 40 }, 396 + "id": 30, 397 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 398 + "title": "PDS Request Rate by Method", 399 + "type": "timeseries", 400 + "targets": [ 401 + { 402 + "expr": "sum by (method) (rate(arabica_pds_requests_total[5m]))", 403 + "legendFormat": "{{method}}", 404 + "refId": "A" 405 + } 406 + ] 407 + }, 408 + { 409 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 410 + "fieldConfig": { 411 + "defaults": { 412 + "color": { "mode": "palette-classic" }, 413 + "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 }, 414 + "unit": "s" 415 + } 416 + }, 417 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 40 }, 418 + "id": 31, 419 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 420 + "title": "PDS Latency by Method (p95)", 421 + "type": "timeseries", 422 + "targets": [ 423 + { 424 + "expr": "histogram_quantile(0.95, sum by (le, method) (rate(arabica_pds_request_duration_seconds_bucket[5m])))", 425 + "legendFormat": "{{method}}", 426 + "refId": "A" 427 + } 428 + ] 429 + }, 430 + { 431 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 432 + "fieldConfig": { 433 + "defaults": { 434 + "color": { "mode": "palette-classic" }, 435 + "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2, "stacking": { "mode": "normal" } } 436 + } 437 + }, 438 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 48 }, 439 + "id": 32, 440 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 441 + "title": "PDS Requests by Collection", 442 + "type": "timeseries", 443 + "targets": [ 444 + { 445 + "expr": "sum by (collection) (rate(arabica_pds_requests_total[5m]))", 446 + "legendFormat": "{{collection}}", 447 + "refId": "A" 448 + } 449 + ] 450 + }, 451 + { 452 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 453 + "fieldConfig": { 454 + "defaults": { 455 + "color": { "mode": "thresholds" }, 456 + "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 }, 457 + "thresholds": { "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 0.1 }] } 458 + } 459 + }, 460 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 48 }, 461 + "id": 33, 462 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 463 + "title": "PDS Error Rate by Method", 464 + "type": "timeseries", 465 + "targets": [ 466 + { 467 + "expr": "sum by (method) (rate(arabica_pds_errors_total[5m]))", 468 + "legendFormat": "{{method}}", 469 + "refId": "A" 470 + } 471 + ] 472 + }, 473 + { 474 + "collapsed": false, 475 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 56 }, 476 + "id": 104, 477 + "title": "Feed Cache", 478 + "type": "row" 479 + }, 480 + { 481 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 482 + "fieldConfig": { 483 + "defaults": { 484 + "color": { "mode": "palette-classic" }, 485 + "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2 } 486 + } 487 + }, 488 + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 57 }, 489 + "id": 40, 490 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 491 + "title": "Feed Cache Hits vs Misses", 492 + "type": "timeseries", 493 + "targets": [ 494 + { 495 + "expr": "rate(arabica_feed_cache_hits_total[5m])", 496 + "legendFormat": "hits", 497 + "refId": "A" 498 + }, 499 + { 500 + "expr": "rate(arabica_feed_cache_misses_total[5m])", 501 + "legendFormat": "misses", 502 + "refId": "B" 503 + } 504 + ] 505 + }, 506 + { 507 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 508 + "fieldConfig": { 509 + "defaults": { 510 + "color": { "mode": "thresholds" }, 511 + "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 }, 512 + "unit": "percentunit", 513 + "thresholds": { "steps": [{ "color": "red", "value": null }, { "color": "yellow", "value": 0.5 }, { "color": "green", "value": 0.8 }] } 514 + } 515 + }, 516 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 57 }, 517 + "id": 41, 518 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 519 + "title": "Feed Cache Hit Rate", 520 + "type": "timeseries", 521 + "targets": [ 522 + { 523 + "expr": "rate(arabica_feed_cache_hits_total[5m]) / (rate(arabica_feed_cache_hits_total[5m]) + rate(arabica_feed_cache_misses_total[5m]))", 524 + "legendFormat": "hit rate", 525 + "refId": "A" 526 + } 527 + ] 528 + }, 529 + { 530 + "collapsed": false, 531 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 65 }, 532 + "id": 105, 533 + "title": "Users", 534 + "type": "row" 535 + }, 536 + { 537 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 538 + "fieldConfig": { 539 + "defaults": { 540 + "color": { "mode": "thresholds" }, 541 + "thresholds": { "steps": [{ "color": "blue", "value": null }] } 542 + } 543 + }, 544 + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 66 }, 545 + "id": 50, 546 + "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }, 547 + "title": "Known Users", 548 + "type": "stat", 549 + "targets": [ 550 + { 551 + "expr": "arabica_known_users_total", 552 + "legendFormat": "", 553 + "refId": "A" 554 + } 555 + ] 556 + }, 557 + { 558 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 559 + "fieldConfig": { 560 + "defaults": { 561 + "color": { "mode": "thresholds" }, 562 + "thresholds": { "steps": [{ "color": "blue", "value": null }] } 563 + } 564 + }, 565 + "gridPos": { "h": 4, "w": 6, "x": 6, "y": 66 }, 566 + "id": 51, 567 + "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }, 568 + "title": "Registered Users", 569 + "type": "stat", 570 + "targets": [ 571 + { 572 + "expr": "arabica_registered_users_total", 573 + "legendFormat": "", 574 + "refId": "A" 575 + } 576 + ] 577 + }, 578 + { 579 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 580 + "fieldConfig": { 581 + "defaults": { 582 + "color": { "mode": "palette-classic" }, 583 + "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 } 584 + } 585 + }, 586 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 66 }, 587 + "id": 52, 588 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 589 + "title": "Login Rate", 590 + "type": "timeseries", 591 + "targets": [ 592 + { 593 + "expr": "rate(arabica_auth_logins_total{status=\"success\"}[5m])", 594 + "legendFormat": "success", 595 + "refId": "A" 596 + }, 597 + { 598 + "expr": "rate(arabica_auth_logins_total{status=\"failure\"}[5m])", 599 + "legendFormat": "failure", 600 + "refId": "B" 601 + } 602 + ] 603 + }, 604 + { 605 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 606 + "fieldConfig": { 607 + "defaults": { 608 + "color": { "mode": "thresholds" }, 609 + "thresholds": { "steps": [{ "color": "blue", "value": null }] } 610 + } 611 + }, 612 + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 70 }, 613 + "id": 53, 614 + "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }, 615 + "title": "Indexed Records", 616 + "type": "stat", 617 + "targets": [ 618 + { 619 + "expr": "arabica_indexed_records_total", 620 + "legendFormat": "", 621 + "refId": "A" 622 + } 623 + ] 624 + }, 625 + { 626 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 627 + "fieldConfig": { 628 + "defaults": { 629 + "color": { "mode": "palette-classic" }, 630 + "custom": { "drawStyle": "bars", "fillOpacity": 80, "lineWidth": 1 } 631 + } 632 + }, 633 + "gridPos": { "h": 4, "w": 6, "x": 6, "y": 70 }, 634 + "id": 54, 635 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 636 + "title": "Records by Collection", 637 + "type": "timeseries", 638 + "targets": [ 639 + { 640 + "expr": "arabica_indexed_records_by_collection", 641 + "legendFormat": "{{collection}}", 642 + "refId": "A" 643 + } 644 + ] 645 + }, 646 + { 647 + "collapsed": false, 648 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 74 }, 649 + "id": 106, 650 + "title": "Engagement", 651 + "type": "row" 652 + }, 653 + { 654 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 655 + "fieldConfig": { 656 + "defaults": { 657 + "color": { "mode": "thresholds" }, 658 + "thresholds": { "steps": [{ "color": "purple", "value": null }] } 659 + } 660 + }, 661 + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 75 }, 662 + "id": 60, 663 + "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }, 664 + "title": "Total Likes", 665 + "type": "stat", 666 + "targets": [ 667 + { 668 + "expr": "arabica_indexed_likes_total", 669 + "legendFormat": "", 670 + "refId": "A" 671 + } 672 + ] 673 + }, 674 + { 675 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 676 + "fieldConfig": { 677 + "defaults": { 678 + "color": { "mode": "thresholds" }, 679 + "thresholds": { "steps": [{ "color": "purple", "value": null }] } 680 + } 681 + }, 682 + "gridPos": { "h": 4, "w": 6, "x": 6, "y": 75 }, 683 + "id": 61, 684 + "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }, 685 + "title": "Total Comments", 686 + "type": "stat", 687 + "targets": [ 688 + { 689 + "expr": "arabica_indexed_comments_total", 690 + "legendFormat": "", 691 + "refId": "A" 692 + } 693 + ] 694 + }, 695 + { 696 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 697 + "fieldConfig": { 698 + "defaults": { 699 + "color": { "mode": "palette-classic" }, 700 + "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2 } 701 + } 702 + }, 703 + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 75 }, 704 + "id": 62, 705 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 706 + "title": "Like & Comment Rate", 707 + "type": "timeseries", 708 + "targets": [ 709 + { 710 + "expr": "sum by (operation) (rate(arabica_likes_total[5m]))", 711 + "legendFormat": "likes ({{operation}})", 712 + "refId": "A" 713 + }, 714 + { 715 + "expr": "sum by (operation) (rate(arabica_comments_total[5m]))", 716 + "legendFormat": "comments ({{operation}})", 717 + "refId": "B" 718 + } 719 + ] 720 + }, 721 + { 722 + "collapsed": false, 723 + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 83 }, 724 + "id": 107, 725 + "title": "Community", 726 + "type": "row" 727 + }, 728 + { 729 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 730 + "fieldConfig": { 731 + "defaults": { 732 + "color": { "mode": "thresholds" }, 733 + "thresholds": { "steps": [{ "color": "orange", "value": null }] } 734 + } 735 + }, 736 + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 84 }, 737 + "id": 70, 738 + "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"] } }, 739 + "title": "Pending Join Requests", 740 + "type": "stat", 741 + "targets": [ 742 + { 743 + "expr": "arabica_join_requests_pending", 744 + "legendFormat": "", 745 + "refId": "A" 746 + } 747 + ] 748 + }, 749 + { 750 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 751 + "fieldConfig": { 752 + "defaults": { 753 + "color": { "mode": "palette-classic" }, 754 + "custom": { "drawStyle": "line", "fillOpacity": 20, "lineWidth": 2 } 755 + } 756 + }, 757 + "gridPos": { "h": 8, "w": 12, "x": 6, "y": 84 }, 758 + "id": 71, 759 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 760 + "title": "Join Requests & Invites Rate", 761 + "type": "timeseries", 762 + "targets": [ 763 + { 764 + "expr": "rate(arabica_join_requests_total[5m])", 765 + "legendFormat": "join requests", 766 + "refId": "A" 767 + }, 768 + { 769 + "expr": "rate(arabica_invites_created_total[5m])", 770 + "legendFormat": "invites sent", 771 + "refId": "B" 772 + } 773 + ] 774 + }, 775 + { 776 + "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, 777 + "fieldConfig": { 778 + "defaults": { 779 + "color": { "mode": "palette-classic" }, 780 + "custom": { "drawStyle": "line", "fillOpacity": 10, "lineWidth": 2 } 781 + } 782 + }, 783 + "gridPos": { "h": 8, "w": 6, "x": 18, "y": 84 }, 784 + "id": 72, 785 + "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } }, 786 + "title": "Reports Rate", 787 + "type": "timeseries", 788 + "targets": [ 789 + { 790 + "expr": "rate(arabica_reports_total[5m])", 791 + "legendFormat": "reports/s", 792 + "refId": "A" 793 + } 794 + ] 795 + } 796 + ], 797 + "schemaVersion": 39, 798 + "tags": ["arabica", "prometheus", "metrics"], 799 + "templating": { "list": [] }, 800 + "time": { "from": "now-1h", "to": "now" }, 801 + "timepicker": {}, 802 + "timezone": "", 803 + "title": "Arabica - Prometheus Metrics", 804 + "uid": "arabica-prometheus", 805 + "version": 1 806 + }
+17
internal/atproto/client.go
··· 5 "fmt" 6 "time" 7 8 "github.com/bluesky-social/indigo/atproto/atclient" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/rs/zerolog/log" ··· 80 err = apiClient.Post(ctx, "com.atproto.repo.createRecord", body, &result) 81 82 duration := time.Since(start) 83 84 if err != nil { 85 log.Error(). 86 Err(err). 87 Str("method", "createRecord"). ··· 146 err = apiClient.Get(ctx, "com.atproto.repo.getRecord", params, &result) 147 148 duration := time.Since(start) 149 150 if err != nil { 151 log.Error(). 152 Err(err). 153 Str("method", "getRecord"). ··· 232 233 duration := time.Since(start) 234 recordCount := len(result.Records) 235 236 if err != nil { 237 log.Error(). 238 Err(err). 239 Str("method", "listRecords"). ··· 366 err = apiClient.Post(ctx, "com.atproto.repo.putRecord", body, &result) 367 368 duration := time.Since(start) 369 370 if err != nil { 371 log.Error(). 372 Err(err). 373 Str("method", "putRecord"). ··· 420 err = apiClient.Post(ctx, "com.atproto.repo.deleteRecord", body, &result) 421 422 duration := time.Since(start) 423 424 if err != nil { 425 log.Error(). 426 Err(err). 427 Str("method", "deleteRecord").
··· 5 "fmt" 6 "time" 7 8 + "arabica/internal/metrics" 9 + 10 "github.com/bluesky-social/indigo/atproto/atclient" 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 "github.com/rs/zerolog/log" ··· 82 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 + metrics.PDSRequestsTotal.WithLabelValues("createRecord", input.Collection).Inc() 87 88 if err != nil { 89 + metrics.PDSErrorsTotal.WithLabelValues("createRecord").Inc() 90 log.Error(). 91 Err(err). 92 Str("method", "createRecord"). ··· 151 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 + metrics.PDSRequestsTotal.WithLabelValues("getRecord", input.Collection).Inc() 156 157 if err != nil { 158 + metrics.PDSErrorsTotal.WithLabelValues("getRecord").Inc() 159 log.Error(). 160 Err(err). 161 Str("method", "getRecord"). ··· 240 241 duration := time.Since(start) 242 recordCount := len(result.Records) 243 + metrics.PDSRequestDuration.WithLabelValues("listRecords").Observe(duration.Seconds()) 244 + metrics.PDSRequestsTotal.WithLabelValues("listRecords", input.Collection).Inc() 245 246 if err != nil { 247 + metrics.PDSErrorsTotal.WithLabelValues("listRecords").Inc() 248 log.Error(). 249 Err(err). 250 Str("method", "listRecords"). ··· 377 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 + metrics.PDSRequestsTotal.WithLabelValues("putRecord", input.Collection).Inc() 382 383 if err != nil { 384 + metrics.PDSErrorsTotal.WithLabelValues("putRecord").Inc() 385 log.Error(). 386 Err(err). 387 Str("method", "putRecord"). ··· 434 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 + metrics.PDSRequestsTotal.WithLabelValues("deleteRecord", input.Collection).Inc() 439 440 if err != nil { 441 + metrics.PDSErrorsTotal.WithLabelValues("deleteRecord").Inc() 442 log.Error(). 443 Err(err). 444 Str("method", "deleteRecord").
+3
internal/feed/service.go
··· 8 9 "arabica/internal/atproto" 10 "arabica/internal/lexicons" 11 "arabica/internal/models" 12 13 "github.com/rs/zerolog/log" ··· 216 s.cache.mu.RUnlock() 217 218 if cacheValid { 219 // Apply moderation filtering to cached items 220 // This ensures recently hidden content doesn't appear 221 items = s.filterModeratedItems(ctx, items) ··· 247 return items, nil 248 } 249 250 log.Debug().Msg("feed: refreshing public feed cache") 251 252 // Fetch PublicFeedCacheSize items to cache (20 items)
··· 8 9 "arabica/internal/atproto" 10 "arabica/internal/lexicons" 11 + "arabica/internal/metrics" 12 "arabica/internal/models" 13 14 "github.com/rs/zerolog/log" ··· 217 s.cache.mu.RUnlock() 218 219 if cacheValid { 220 + metrics.FeedCacheHitsTotal.Inc() 221 // Apply moderation filtering to cached items 222 // This ensures recently hidden content doesn't appear 223 items = s.filterModeratedItems(ctx, items) ··· 249 return items, nil 250 } 251 252 + metrics.FeedCacheMissesTotal.Inc() 253 log.Debug().Msg("feed: refreshing public feed cache") 254 255 // Fetch PublicFeedCacheSize items to cache (20 items)
+7
internal/firehose/consumer.go
··· 10 "sync/atomic" 11 "time" 12 13 "github.com/gorilla/websocket" 14 "github.com/klauspost/compress/zstd" 15 "github.com/rs/zerolog/log" ··· 185 c.connMu.Unlock() 186 187 c.connected.Store(true) 188 log.Info().Str("endpoint", endpoint).Msg("firehose: connected to Jetstream") 189 190 // Mark index as ready once connected ··· 198 } 199 c.connMu.Unlock() 200 c.connected.Store(false) 201 }() 202 203 // Read events ··· 221 c.bytesReceived.Add(int64(len(message))) 222 223 if err := c.processMessage(message); err != nil { 224 log.Warn().Err(err).Msg("firehose: failed to process message") 225 } 226 } ··· 309 if !strings.HasPrefix(commit.Collection, "social.arabica.alpha.") { 310 return nil 311 } 312 313 log.Debug(). 314 Str("did", event.DID).
··· 10 "sync/atomic" 11 "time" 12 13 + "arabica/internal/metrics" 14 + 15 "github.com/gorilla/websocket" 16 "github.com/klauspost/compress/zstd" 17 "github.com/rs/zerolog/log" ··· 187 c.connMu.Unlock() 188 189 c.connected.Store(true) 190 + metrics.FirehoseConnectionState.Set(1) 191 log.Info().Str("endpoint", endpoint).Msg("firehose: connected to Jetstream") 192 193 // Mark index as ready once connected ··· 201 } 202 c.connMu.Unlock() 203 c.connected.Store(false) 204 + metrics.FirehoseConnectionState.Set(0) 205 }() 206 207 // Read events ··· 225 c.bytesReceived.Add(int64(len(message))) 226 227 if err := c.processMessage(message); err != nil { 228 + metrics.FirehoseErrorsTotal.Inc() 229 log.Warn().Err(err).Msg("firehose: failed to process message") 230 } 231 } ··· 314 if !strings.HasPrefix(commit.Collection, "social.arabica.alpha.") { 315 return nil 316 } 317 + 318 + metrics.FirehoseEventsTotal.WithLabelValues(commit.Collection, commit.Operation).Inc() 319 320 log.Debug(). 321 Str("did", event.DID).
+50
internal/firehose/index.go
··· 959 return count 960 } 961 962 // Helper functions 963 964 func makeTimeKey(t time.Time, uri string) []byte {
··· 959 return count 960 } 961 962 + // KnownDIDCount returns the number of unique DIDs in the index 963 + func (idx *FeedIndex) KnownDIDCount() int { 964 + var count int 965 + _ = idx.db.View(func(tx *bolt.Tx) error { 966 + b := tx.Bucket(BucketKnownDIDs) 967 + count = b.Stats().KeyN 968 + return nil 969 + }) 970 + return count 971 + } 972 + 973 + // TotalLikeCount returns the total number of likes indexed 974 + func (idx *FeedIndex) TotalLikeCount() int { 975 + var count int 976 + _ = idx.db.View(func(tx *bolt.Tx) error { 977 + b := tx.Bucket(BucketLikes) 978 + count = b.Stats().KeyN 979 + return nil 980 + }) 981 + return count 982 + } 983 + 984 + // TotalCommentCount returns the total number of comments indexed 985 + func (idx *FeedIndex) TotalCommentCount() int { 986 + var count int 987 + _ = idx.db.View(func(tx *bolt.Tx) error { 988 + b := tx.Bucket(BucketCommentsByActor) 989 + count = b.Stats().KeyN 990 + return nil 991 + }) 992 + return count 993 + } 994 + 995 + // RecordCountByCollection returns a breakdown of record counts by collection type 996 + func (idx *FeedIndex) RecordCountByCollection() map[string]int { 997 + counts := make(map[string]int) 998 + _ = idx.db.View(func(tx *bolt.Tx) error { 999 + records := tx.Bucket(BucketRecords) 1000 + return records.ForEach(func(k, v []byte) error { 1001 + var record IndexedRecord 1002 + if err := json.Unmarshal(v, &record); err != nil { 1003 + return nil 1004 + } 1005 + counts[record.Collection]++ 1006 + return nil 1007 + }) 1008 + }) 1009 + return counts 1010 + } 1011 + 1012 // Helper functions 1013 1014 func makeTimeKey(t time.Time, uri string) []byte {
+71
internal/handlers/admin.go
··· 7 8 "arabica/internal/atproto" 9 "arabica/internal/database/boltstore" 10 "arabica/internal/middleware" 11 "arabica/internal/moderation" 12 "arabica/internal/web/components" 13 "arabica/internal/web/pages" 14 15 "github.com/bluesky-social/indigo/atproto/syntax" 16 "github.com/rs/zerolog/log" 17 ) 18 ··· 195 joinRequests, _ = h.joinStore.ListRequests() 196 } 197 198 return pages.AdminProps{ 199 HiddenRecords: hiddenRecords, 200 AuditLog: auditLog, 201 Reports: enrichedReports, 202 BlockedUsers: blockedUsers, 203 JoinRequests: joinRequests, 204 CanHide: canHide, 205 CanUnhide: canUnhide, 206 CanViewLogs: canViewLogs, ··· 589 w.Header().Set("HX-Trigger", "mod-action") 590 w.WriteHeader(http.StatusOK) 591 }
··· 7 8 "arabica/internal/atproto" 9 "arabica/internal/database/boltstore" 10 + "arabica/internal/metrics" 11 "arabica/internal/middleware" 12 "arabica/internal/moderation" 13 "arabica/internal/web/components" 14 "arabica/internal/web/pages" 15 16 "github.com/bluesky-social/indigo/atproto/syntax" 17 + "github.com/prometheus/client_golang/prometheus" 18 + dto "github.com/prometheus/client_model/go" 19 "github.com/rs/zerolog/log" 20 ) 21 ··· 198 joinRequests, _ = h.joinStore.ListRequests() 199 } 200 201 + // Build stats for admin users 202 + var stats pages.AdminStats 203 + if isAdmin { 204 + stats = h.collectAdminStats() 205 + } 206 + 207 return pages.AdminProps{ 208 HiddenRecords: hiddenRecords, 209 AuditLog: auditLog, 210 Reports: enrichedReports, 211 BlockedUsers: blockedUsers, 212 JoinRequests: joinRequests, 213 + Stats: stats, 214 CanHide: canHide, 215 CanUnhide: canUnhide, 216 CanViewLogs: canViewLogs, ··· 599 w.Header().Set("HX-Trigger", "mod-action") 600 w.WriteHeader(http.StatusOK) 601 } 602 + 603 + // collectAdminStats gathers current system statistics from available data sources. 604 + func (h *Handler) collectAdminStats() pages.AdminStats { 605 + var stats pages.AdminStats 606 + 607 + if h.feedIndex != nil { 608 + stats.KnownUsers = h.feedIndex.KnownDIDCount() 609 + stats.IndexedRecords = h.feedIndex.RecordCount() 610 + stats.TotalLikes = h.feedIndex.TotalLikeCount() 611 + stats.TotalComments = h.feedIndex.TotalCommentCount() 612 + stats.RecordsByCollection = h.feedIndex.RecordCountByCollection() 613 + } 614 + 615 + if h.feedRegistry != nil { 616 + stats.RegisteredUsers = h.feedRegistry.Count() 617 + } 618 + 619 + if h.joinStore != nil { 620 + if reqs, err := h.joinStore.ListRequests(); err == nil { 621 + stats.PendingJoinRequests = len(reqs) 622 + } 623 + } 624 + 625 + // Read firehose connection state from the Prometheus gauge 626 + stats.FirehoseConnected = getGaugeValue(metrics.FirehoseConnectionState) == 1 627 + 628 + return stats 629 + } 630 + 631 + // getGaugeValue reads the current value of a prometheus.Gauge. 632 + func getGaugeValue(g prometheus.Gauge) float64 { 633 + m := &dto.Metric{} 634 + if err := g.Write(m); err != nil { 635 + return 0 636 + } 637 + if m.Gauge != nil { 638 + return m.GetGauge().GetValue() 639 + } 640 + return 0 641 + } 642 + 643 + // HandleAdminStats renders the stats partial for HTMX refresh. 644 + func (h *Handler) HandleAdminStats(w http.ResponseWriter, r *http.Request) { 645 + userDID, err := atproto.GetAuthenticatedDID(r.Context()) 646 + if err != nil || userDID == "" { 647 + http.Error(w, "Authentication required", http.StatusUnauthorized) 648 + return 649 + } 650 + 651 + if h.moderationService == nil || !h.moderationService.IsAdmin(userDID) { 652 + http.Error(w, "Access denied", http.StatusForbidden) 653 + return 654 + } 655 + 656 + stats := h.collectAdminStats() 657 + 658 + if err := pages.AdminStatsContent(stats).Render(r.Context(), w); err != nil { 659 + log.Error().Err(err).Msg("Failed to render admin stats partial") 660 + http.Error(w, "Failed to render", http.StatusInternalServerError) 661 + } 662 + }
+5
internal/handlers/auth.go
··· 7 "net/http" 8 "time" 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 "github.com/rs/zerolog/log" 12 ) ··· 65 // Process the callback with all query parameters 66 sessData, err := h.oauth.HandleCallback(r.Context(), r.URL.Query()) 67 if err != nil { 68 log.Error().Err(err).Msg("Failed to complete OAuth flow") 69 http.Error(w, "Failed to complete login", http.StatusInternalServerError) 70 return ··· 95 SameSite: http.SameSiteLaxMode, 96 MaxAge: 86400 * 30, // 30 days 97 }) 98 99 log.Info(). 100 Str("user_did", sessData.AccountDID.String()).
··· 7 "net/http" 8 "time" 9 10 + "arabica/internal/metrics" 11 + 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 "github.com/rs/zerolog/log" 14 ) ··· 67 // Process the callback with all query parameters 68 sessData, err := h.oauth.HandleCallback(r.Context(), r.URL.Query()) 69 if err != nil { 70 + metrics.AuthLoginsTotal.WithLabelValues("failure").Inc() 71 log.Error().Err(err).Msg("Failed to complete OAuth flow") 72 http.Error(w, "Failed to complete login", http.StatusInternalServerError) 73 return ··· 98 SameSite: http.SameSiteLaxMode, 99 MaxAge: 86400 * 30, // 30 days 100 }) 101 + 102 + metrics.AuthLoginsTotal.WithLabelValues("success").Inc() 103 104 log.Info(). 105 Str("user_did", sessData.AccountDID.String()).
+3
internal/handlers/feed.go
··· 7 "arabica/internal/atproto" 8 "arabica/internal/feed" 9 "arabica/internal/lexicons" 10 "arabica/internal/models" 11 "arabica/internal/moderation" 12 "arabica/internal/web/components" ··· 185 return 186 } 187 isLiked = false 188 189 // Update firehose index 190 if h.feedIndex != nil { ··· 204 return 205 } 206 isLiked = true 207 208 // Update firehose index 209 if h.feedIndex != nil {
··· 7 "arabica/internal/atproto" 8 "arabica/internal/feed" 9 "arabica/internal/lexicons" 10 + "arabica/internal/metrics" 11 "arabica/internal/models" 12 "arabica/internal/moderation" 13 "arabica/internal/web/components" ··· 186 return 187 } 188 isLiked = false 189 + metrics.LikesTotal.WithLabelValues("delete").Inc() 190 191 // Update firehose index 192 if h.feedIndex != nil { ··· 206 return 207 } 208 isLiked = true 209 + metrics.LikesTotal.WithLabelValues("create").Inc() 210 211 // Update firehose index 212 if h.feedIndex != nil {
+5
internal/handlers/handlers.go
··· 13 "arabica/internal/email" 14 "arabica/internal/feed" 15 "arabica/internal/firehose" 16 "arabica/internal/middleware" 17 "arabica/internal/models" 18 "arabica/internal/moderation" ··· 315 return 316 } 317 318 // Update firehose index (pass parent URI and comment's CID for threading) 319 if h.feedIndex != nil { 320 _ = h.feedIndex.UpsertComment(didStr, comment.RKey, subjectURI, parentURI, comment.CID, text, comment.CreatedAt) ··· 390 log.Error().Err(err).Msg("Failed to delete comment") 391 return 392 } 393 394 // Update firehose index 395 if h.feedIndex != nil {
··· 13 "arabica/internal/email" 14 "arabica/internal/feed" 15 "arabica/internal/firehose" 16 + "arabica/internal/metrics" 17 "arabica/internal/middleware" 18 "arabica/internal/models" 19 "arabica/internal/moderation" ··· 316 return 317 } 318 319 + metrics.CommentsTotal.WithLabelValues("create").Inc() 320 + 321 // Update firehose index (pass parent URI and comment's CID for threading) 322 if h.feedIndex != nil { 323 _ = h.feedIndex.UpsertComment(didStr, comment.RKey, subjectURI, parentURI, comment.CID, text, comment.CreatedAt) ··· 393 log.Error().Err(err).Msg("Failed to delete comment") 394 return 395 } 396 + 397 + metrics.CommentsTotal.WithLabelValues("delete").Inc() 398 399 // Update firehose index 400 if h.feedIndex != nil {
+3
internal/handlers/join.go
··· 9 10 "arabica/internal/atproto" 11 "arabica/internal/database/boltstore" 12 "arabica/internal/middleware" 13 "arabica/internal/moderation" 14 "arabica/internal/web/pages" ··· 66 http.Error(w, "Failed to save request, please try again", http.StatusInternalServerError) 67 return 68 } 69 log.Info().Str("email", emailAddr).Msg("Join request saved") 70 } 71 ··· 146 return 147 } 148 149 log.Info().Str("email", reqEmail).Str("code", out.Code).Str("by", userDID).Msg("Invite code created") 150 151 // Email the invite code to the requester
··· 9 10 "arabica/internal/atproto" 11 "arabica/internal/database/boltstore" 12 + "arabica/internal/metrics" 13 "arabica/internal/middleware" 14 "arabica/internal/moderation" 15 "arabica/internal/web/pages" ··· 67 http.Error(w, "Failed to save request, please try again", http.StatusInternalServerError) 68 return 69 } 70 + metrics.JoinRequestsTotal.Inc() 71 log.Info().Str("email", emailAddr).Msg("Join request saved") 72 } 73 ··· 148 return 149 } 150 151 + metrics.InvitesCreatedTotal.Inc() 152 log.Info().Str("email", reqEmail).Str("code", out.Code).Str("by", userDID).Msg("Invite code created") 153 154 // Email the invite code to the requester
+3
internal/handlers/report.go
··· 9 "time" 10 11 "arabica/internal/atproto" 12 "arabica/internal/moderation" 13 14 "github.com/rs/zerolog/log" ··· 141 writeReportError(w, "Failed to save report", http.StatusInternalServerError) 142 return 143 } 144 145 log.Info(). 146 Str("report_id", report.ID).
··· 9 "time" 10 11 "arabica/internal/atproto" 12 + "arabica/internal/metrics" 13 "arabica/internal/moderation" 14 15 "github.com/rs/zerolog/log" ··· 142 writeReportError(w, "Failed to save report", http.StatusInternalServerError) 143 return 144 } 145 + 146 + metrics.ReportsTotal.Inc() 147 148 log.Info(). 149 Str("report_id", report.ID).
+77
internal/metrics/collector.go
···
··· 1 + package metrics 2 + 3 + import ( 4 + "context" 5 + "time" 6 + 7 + "github.com/rs/zerolog/log" 8 + ) 9 + 10 + // StatsSource provides functions to retrieve current counts for gauge metrics. 11 + // Each function returns the current count; returning -1 indicates the source is unavailable. 12 + type StatsSource struct { 13 + KnownDIDCount func() int 14 + RegisteredCount func() int 15 + RecordCount func() int 16 + PendingJoinCount func() int 17 + LikeCount func() int 18 + CommentCount func() int 19 + RecordCountByCollection func() map[string]int 20 + FirehoseConnected func() bool 21 + } 22 + 23 + // StartCollector launches a goroutine that periodically updates gauge metrics. 24 + // It runs every interval until the context is cancelled. 25 + func StartCollector(ctx context.Context, src StatsSource, interval time.Duration) { 26 + // Do an initial collection immediately 27 + collect(src) 28 + 29 + go func() { 30 + ticker := time.NewTicker(interval) 31 + defer ticker.Stop() 32 + 33 + for { 34 + select { 35 + case <-ctx.Done(): 36 + return 37 + case <-ticker.C: 38 + collect(src) 39 + } 40 + } 41 + }() 42 + 43 + log.Info().Dur("interval", interval).Msg("Metrics collector started") 44 + } 45 + 46 + func collect(src StatsSource) { 47 + if src.KnownDIDCount != nil { 48 + KnownUsersTotal.Set(float64(src.KnownDIDCount())) 49 + } 50 + if src.RegisteredCount != nil { 51 + RegisteredUsersTotal.Set(float64(src.RegisteredCount())) 52 + } 53 + if src.RecordCount != nil { 54 + IndexedRecordsTotal.Set(float64(src.RecordCount())) 55 + } 56 + if src.PendingJoinCount != nil { 57 + JoinRequestsPending.Set(float64(src.PendingJoinCount())) 58 + } 59 + if src.LikeCount != nil { 60 + IndexedLikesTotal.Set(float64(src.LikeCount())) 61 + } 62 + if src.CommentCount != nil { 63 + IndexedCommentsTotal.Set(float64(src.CommentCount())) 64 + } 65 + if src.RecordCountByCollection != nil { 66 + for collection, count := range src.RecordCountByCollection() { 67 + IndexedRecordsByCollection.WithLabelValues(collection).Set(float64(count)) 68 + } 69 + } 70 + if src.FirehoseConnected != nil { 71 + if src.FirehoseConnected() { 72 + FirehoseConnectionState.Set(1) 73 + } else { 74 + FirehoseConnectionState.Set(0) 75 + } 76 + } 77 + }
+222
internal/metrics/metrics.go
···
··· 1 + package metrics 2 + 3 + import ( 4 + "github.com/prometheus/client_golang/prometheus" 5 + "github.com/prometheus/client_golang/prometheus/promauto" 6 + ) 7 + 8 + // HTTP metrics 9 + var ( 10 + HTTPRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ 11 + Name: "arabica_http_requests_total", 12 + Help: "Total number of HTTP requests", 13 + }, []string{"method", "path", "status"}) 14 + 15 + HTTPRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ 16 + Name: "arabica_http_request_duration_seconds", 17 + Help: "HTTP request duration in seconds", 18 + Buckets: []float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5}, 19 + }, []string{"method", "path"}) 20 + ) 21 + 22 + // Firehose metrics 23 + var ( 24 + FirehoseEventsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ 25 + Name: "arabica_firehose_events_total", 26 + Help: "Total number of firehose events processed", 27 + }, []string{"collection", "operation"}) 28 + 29 + FirehoseConnectionState = promauto.NewGauge(prometheus.GaugeOpts{ 30 + Name: "arabica_firehose_connection_state", 31 + Help: "Firehose connection state (1=connected, 0=disconnected)", 32 + }) 33 + 34 + FirehoseErrorsTotal = promauto.NewCounter(prometheus.CounterOpts{ 35 + Name: "arabica_firehose_errors_total", 36 + Help: "Total number of firehose processing errors", 37 + }) 38 + ) 39 + 40 + // PDS metrics 41 + var ( 42 + PDSRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ 43 + Name: "arabica_pds_requests_total", 44 + Help: "Total number of PDS requests", 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 + ) 58 + 59 + // Feed metrics 60 + var ( 61 + FeedCacheHitsTotal = promauto.NewCounter(prometheus.CounterOpts{ 62 + Name: "arabica_feed_cache_hits_total", 63 + Help: "Total number of feed cache hits", 64 + }) 65 + 66 + FeedCacheMissesTotal = promauto.NewCounter(prometheus.CounterOpts{ 67 + Name: "arabica_feed_cache_misses_total", 68 + Help: "Total number of feed cache misses", 69 + }) 70 + ) 71 + 72 + // Business metrics (gauges updated periodically by collector) 73 + var ( 74 + KnownUsersTotal = promauto.NewGauge(prometheus.GaugeOpts{ 75 + Name: "arabica_known_users_total", 76 + Help: "Total number of unique DIDs in the index", 77 + }) 78 + 79 + RegisteredUsersTotal = promauto.NewGauge(prometheus.GaugeOpts{ 80 + Name: "arabica_registered_users_total", 81 + Help: "Total number of registered feed users", 82 + }) 83 + 84 + IndexedRecordsTotal = promauto.NewGauge(prometheus.GaugeOpts{ 85 + Name: "arabica_indexed_records_total", 86 + Help: "Total number of indexed records", 87 + }) 88 + 89 + JoinRequestsPending = promauto.NewGauge(prometheus.GaugeOpts{ 90 + Name: "arabica_join_requests_pending", 91 + Help: "Number of pending join requests", 92 + }) 93 + 94 + IndexedLikesTotal = promauto.NewGauge(prometheus.GaugeOpts{ 95 + Name: "arabica_indexed_likes_total", 96 + Help: "Total number of indexed likes", 97 + }) 98 + 99 + IndexedCommentsTotal = promauto.NewGauge(prometheus.GaugeOpts{ 100 + Name: "arabica_indexed_comments_total", 101 + Help: "Total number of indexed comments", 102 + }) 103 + 104 + IndexedRecordsByCollection = promauto.NewGaugeVec(prometheus.GaugeOpts{ 105 + Name: "arabica_indexed_records_by_collection", 106 + Help: "Number of indexed records by collection type", 107 + }, []string{"collection"}) 108 + ) 109 + 110 + // Event counters (incremented on occurrence) 111 + var ( 112 + AuthLoginsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ 113 + Name: "arabica_auth_logins_total", 114 + Help: "Total number of login attempts", 115 + }, []string{"status"}) 116 + 117 + JoinRequestsTotal = promauto.NewCounter(prometheus.CounterOpts{ 118 + Name: "arabica_join_requests_total", 119 + Help: "Total number of join request submissions", 120 + }) 121 + 122 + InvitesCreatedTotal = promauto.NewCounter(prometheus.CounterOpts{ 123 + Name: "arabica_invites_created_total", 124 + Help: "Total number of invites created", 125 + }) 126 + 127 + LikesTotal = promauto.NewCounterVec(prometheus.CounterOpts{ 128 + Name: "arabica_likes_total", 129 + Help: "Total number of like operations", 130 + }, []string{"operation"}) 131 + 132 + CommentsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ 133 + Name: "arabica_comments_total", 134 + Help: "Total number of comment operations", 135 + }, []string{"operation"}) 136 + 137 + ReportsTotal = promauto.NewCounter(prometheus.CounterOpts{ 138 + Name: "arabica_reports_total", 139 + Help: "Total number of user reports submitted", 140 + }) 141 + ) 142 + 143 + // NormalizePath reduces high-cardinality path labels by replacing dynamic 144 + // segments with placeholders. This keeps the metric label space bounded. 145 + func NormalizePath(path string) string { 146 + // Static assets - collapse into one label 147 + if len(path) > 8 && path[:8] == "/static/" { 148 + return "/static/*" 149 + } 150 + 151 + // Known patterns with IDs - normalize the dynamic segments 152 + // Routes like /brews/{id}, /beans/{id}, /profile/{actor}, etc. 153 + segments := splitPath(path) 154 + if len(segments) < 2 { 155 + return path 156 + } 157 + 158 + switch segments[0] { 159 + case "brews": 160 + if len(segments) == 2 { 161 + if segments[1] == "new" || segments[1] == "export" { 162 + return path 163 + } 164 + return "/brews/:id" 165 + } 166 + if len(segments) == 3 && segments[2] == "edit" { 167 + return "/brews/:id/edit" 168 + } 169 + case "beans", "roasters", "grinders", "brewers": 170 + if len(segments) == 2 { 171 + return "/" + segments[0] + "/:id" 172 + } 173 + case "profile": 174 + if len(segments) == 2 { 175 + return "/profile/:actor" 176 + } 177 + case "api": 178 + if len(segments) >= 3 { 179 + switch segments[1] { 180 + case "beans", "roasters", "grinders", "brewers", "comments": 181 + if len(segments) == 3 { 182 + return "/api/" + segments[1] + "/:id" 183 + } 184 + case "profile": 185 + if len(segments) == 3 { 186 + return "/api/profile/:actor" 187 + } 188 + case "modals": 189 + if len(segments) == 4 { 190 + if segments[3] == "new" { 191 + return "/api/modals/" + segments[2] + "/new" 192 + } 193 + return "/api/modals/" + segments[2] + "/:id" 194 + } 195 + } 196 + } 197 + } 198 + 199 + return path 200 + } 201 + 202 + func splitPath(path string) []string { 203 + // Skip leading slash 204 + if len(path) > 0 && path[0] == '/' { 205 + path = path[1:] 206 + } 207 + // Split on / 208 + var segments []string 209 + start := 0 210 + for i := 0; i < len(path); i++ { 211 + if path[i] == '/' { 212 + if i > start { 213 + segments = append(segments, path[start:i]) 214 + } 215 + start = i + 1 216 + } 217 + } 218 + if start < len(path) { 219 + segments = append(segments, path[start:]) 220 + } 221 + return segments 222 + }
+70
internal/metrics/metrics_test.go
···
··· 1 + package metrics 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/stretchr/testify/assert" 7 + ) 8 + 9 + func TestNormalizePath(t *testing.T) { 10 + tests := []struct { 11 + input string 12 + expected string 13 + }{ 14 + // Static assets 15 + {"/static/css/output.css", "/static/*"}, 16 + {"/static/js/app.js", "/static/*"}, 17 + 18 + // Exact routes (no normalization needed) 19 + {"/", "/"}, 20 + {"/about", "/about"}, 21 + {"/terms", "/terms"}, 22 + {"/login", "/login"}, 23 + {"/manage", "/manage"}, 24 + {"/brews", "/brews"}, 25 + 26 + // Brews with IDs 27 + {"/brews/abc123", "/brews/:id"}, 28 + {"/brews/abc123/edit", "/brews/:id/edit"}, 29 + {"/brews/new", "/brews/new"}, 30 + {"/brews/export", "/brews/export"}, 31 + 32 + // Entity record views 33 + {"/beans/abc123", "/beans/:id"}, 34 + {"/roasters/abc123", "/roasters/:id"}, 35 + {"/grinders/abc123", "/grinders/:id"}, 36 + {"/brewers/abc123", "/brewers/:id"}, 37 + 38 + // Profile 39 + {"/profile/someone.bsky.social", "/profile/:actor"}, 40 + 41 + // API entity routes 42 + {"/api/beans/abc123", "/api/beans/:id"}, 43 + {"/api/roasters/abc123", "/api/roasters/:id"}, 44 + {"/api/grinders/abc123", "/api/grinders/:id"}, 45 + {"/api/brewers/abc123", "/api/brewers/:id"}, 46 + {"/api/comments/abc123", "/api/comments/:id"}, 47 + 48 + // API profile 49 + {"/api/profile/someone.bsky.social", "/api/profile/:actor"}, 50 + 51 + // Modal routes 52 + {"/api/modals/bean/new", "/api/modals/bean/new"}, 53 + {"/api/modals/bean/abc123", "/api/modals/bean/:id"}, 54 + {"/api/modals/grinder/new", "/api/modals/grinder/new"}, 55 + {"/api/modals/grinder/abc123", "/api/modals/grinder/:id"}, 56 + 57 + // Routes that shouldn't be normalized 58 + {"/api/feed", "/api/feed"}, 59 + {"/api/brews", "/api/brews"}, 60 + {"/api/manage", "/api/manage"}, 61 + {"/api/resolve-handle", "/api/resolve-handle"}, 62 + {"/metrics", "/metrics"}, 63 + } 64 + 65 + for _, tt := range tests { 66 + t.Run(tt.input, func(t *testing.T) { 67 + assert.Equal(t, tt.expected, NormalizePath(tt.input)) 68 + }) 69 + } 70 + }
+7
internal/middleware/logging.go
··· 3 import ( 4 "net" 5 "net/http" 6 "strings" 7 "time" 8 9 "arabica/internal/atproto" 10 11 "github.com/rs/zerolog" 12 ) ··· 100 logEvent.Interface("headers", headers) 101 102 logEvent.Msg("HTTP request") 103 }) 104 } 105 }
··· 3 import ( 4 "net" 5 "net/http" 6 + "strconv" 7 "strings" 8 "time" 9 10 "arabica/internal/atproto" 11 + "arabica/internal/metrics" 12 13 "github.com/rs/zerolog" 14 ) ··· 102 logEvent.Interface("headers", headers) 103 104 logEvent.Msg("HTTP request") 105 + 106 + // Record Prometheus metrics 107 + normalizedPath := metrics.NormalizePath(r.URL.Path) 108 + metrics.HTTPRequestsTotal.WithLabelValues(r.Method, normalizedPath, strconv.Itoa(rw.statusCode)).Inc() 109 + metrics.HTTPRequestDuration.WithLabelValues(r.Method, normalizedPath).Observe(duration.Seconds()) 110 }) 111 } 112 }
+1
internal/routing/routing.go
··· 121 mux.Handle("POST /_mod/unblock", cop.Handler(http.HandlerFunc(h.HandleUnblockUser))) 122 mux.Handle("POST /_mod/invite", cop.Handler(http.HandlerFunc(h.HandleCreateInvite))) 123 mux.Handle("POST /_mod/dismiss-join", cop.Handler(http.HandlerFunc(h.HandleDismissJoinRequest))) 124 125 // Static files (must come after specific routes) 126 fs := http.FileServer(http.Dir("static"))
··· 121 mux.Handle("POST /_mod/unblock", cop.Handler(http.HandlerFunc(h.HandleUnblockUser))) 122 mux.Handle("POST /_mod/invite", cop.Handler(http.HandlerFunc(h.HandleCreateInvite))) 123 mux.Handle("POST /_mod/dismiss-join", cop.Handler(http.HandlerFunc(h.HandleDismissJoinRequest))) 124 + mux.Handle("GET /_mod/stats", middleware.RequireHTMXMiddleware(http.HandlerFunc(h.HandleAdminStats))) 125 126 // Static files (must come after specific routes) 127 fs := http.FileServer(http.Dir("static"))
+98
internal/web/pages/admin.templ
··· 15 PostContent string // Summary of the reported content 16 } 17 18 type AdminProps struct { 19 HiddenRecords []moderation.HiddenRecord 20 AuditLog []moderation.AuditEntry 21 Reports []EnrichedReport 22 BlockedUsers []moderation.BlacklistedUser 23 JoinRequests []*boltstore.JoinRequest 24 CanHide bool 25 CanUnhide bool 26 CanViewLogs bool ··· 132 </span> 133 } 134 </button> 135 } 136 </nav> 137 <!-- Hidden Records Tab --> ··· 228 </div> 229 </div> 230 } 231 } 232 </div> 233 } ··· 696 </div> 697 </div> 698 }
··· 15 PostContent string // Summary of the reported content 16 } 17 18 + // AdminStats holds aggregate statistics for the admin dashboard 19 + type AdminStats struct { 20 + KnownUsers int 21 + RegisteredUsers int 22 + IndexedRecords int 23 + PendingJoinRequests int 24 + TotalLikes int 25 + TotalComments int 26 + FirehoseConnected bool 27 + RecordsByCollection map[string]int 28 + } 29 + 30 type AdminProps struct { 31 HiddenRecords []moderation.HiddenRecord 32 AuditLog []moderation.AuditEntry 33 Reports []EnrichedReport 34 BlockedUsers []moderation.BlacklistedUser 35 JoinRequests []*boltstore.JoinRequest 36 + Stats AdminStats 37 CanHide bool 38 CanUnhide bool 39 CanViewLogs bool ··· 145 </span> 146 } 147 </button> 148 + <button 149 + type="button" 150 + @click="activeTab = 'stats'" 151 + :class="activeTab === 'stats' ? 'bg-brown-300 text-brown-900 border-brown-400' : 'bg-brown-100 text-brown-600 border-brown-200 hover:bg-brown-200'" 152 + class="px-3 py-1.5 rounded-lg border font-medium text-sm transition-colors" 153 + > 154 + Stats 155 + </button> 156 } 157 </nav> 158 <!-- Hidden Records Tab --> ··· 249 </div> 250 </div> 251 } 252 + } 253 + <!-- Stats Tab (admin only) --> 254 + if props.IsAdmin { 255 + <div x-show="activeTab === 'stats'" x-cloak x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"> 256 + <div 257 + id="stats-panel" 258 + hx-get="/_mod/stats" 259 + hx-trigger="every 30s" 260 + hx-swap="innerHTML" 261 + > 262 + @AdminStatsContent(props.Stats) 263 + </div> 264 + </div> 265 } 266 </div> 267 } ··· 730 </div> 731 </div> 732 } 733 + 734 + templ AdminStatsContent(stats AdminStats) { 735 + <div class="card card-inner"> 736 + <h2 class="section-title">System Stats</h2> 737 + <div class="grid grid-cols-2 md:grid-cols-4 gap-4"> 738 + @statCard("Known Users", fmt.Sprintf("%d", stats.KnownUsers), "Unique DIDs indexed") 739 + @statCard("Registered Users", fmt.Sprintf("%d", stats.RegisteredUsers), "Feed registry") 740 + @statCard("Indexed Records", fmt.Sprintf("%d", stats.IndexedRecords), "Total records") 741 + @statCard("Pending Joins", fmt.Sprintf("%d", stats.PendingJoinRequests), "Join requests") 742 + @statCard("Total Likes", fmt.Sprintf("%d", stats.TotalLikes), "Across all records") 743 + @statCard("Total Comments", fmt.Sprintf("%d", stats.TotalComments), "Across all records") 744 + if stats.FirehoseConnected { 745 + @statCard("Firehose", "Connected", "Real-time events") 746 + } else { 747 + @statCardWarning("Firehose", "Disconnected", "Not receiving events") 748 + } 749 + </div> 750 + if len(stats.RecordsByCollection) > 0 { 751 + <h3 class="section-title mt-6">Records by Collection</h3> 752 + <div class="grid grid-cols-2 md:grid-cols-3 gap-4"> 753 + for collection, count := range stats.RecordsByCollection { 754 + @statCard(collectionLabel(collection), fmt.Sprintf("%d", count), collection) 755 + } 756 + </div> 757 + } 758 + </div> 759 + } 760 + 761 + templ statCard(title, value, subtitle string) { 762 + <div class="bg-brown-50 border border-brown-200 rounded-lg p-4 text-center"> 763 + <div class="text-2xl font-bold text-brown-900">{ value }</div> 764 + <div class="text-sm font-medium text-brown-700 mt-1">{ title }</div> 765 + <div class="text-xs text-brown-500 mt-0.5">{ subtitle }</div> 766 + </div> 767 + } 768 + 769 + templ statCardWarning(title, value, subtitle string) { 770 + <div class="bg-red-50 border border-red-200 rounded-lg p-4 text-center"> 771 + <div class="text-2xl font-bold text-red-700">{ value }</div> 772 + <div class="text-sm font-medium text-red-600 mt-1">{ title }</div> 773 + <div class="text-xs text-red-500 mt-0.5">{ subtitle }</div> 774 + </div> 775 + } 776 + 777 + func collectionLabel(nsid string) string { 778 + switch nsid { 779 + case "social.arabica.alpha.brew": 780 + return "Brews" 781 + case "social.arabica.alpha.bean": 782 + return "Beans" 783 + case "social.arabica.alpha.roaster": 784 + return "Roasters" 785 + case "social.arabica.alpha.grinder": 786 + return "Grinders" 787 + case "social.arabica.alpha.brewer": 788 + return "Brewers" 789 + case "social.arabica.alpha.like": 790 + return "Likes" 791 + case "social.arabica.alpha.comment": 792 + return "Comments" 793 + default: 794 + return nsid 795 + } 796 + }