Skip to content

Devops/ops004 Observation stack intergration: Serilogs with OpenTelemetry, Grafana, Loki, Tempo. Prometheus - #49

Open
the-khiem7 wants to merge 6 commits into
devfrom
devops/OPS004-grafana_otel_serilog
Open

Devops/ops004 Observation stack intergration: Serilogs with OpenTelemetry, Grafana, Loki, Tempo. Prometheus#49
the-khiem7 wants to merge 6 commits into
devfrom
devops/OPS004-grafana_otel_serilog

Conversation

@the-khiem7

@the-khiem7 the-khiem7 commented Feb 13, 2026

Copy link
Copy Markdown
Member

This pull request introduces comprehensive observability support for the SnakeAid.Api project by integrating OpenTelemetry tracing and metrics, enhancing logging with Grafana Loki, and providing a complete Docker Compose stack for local observability (Grafana, Loki, Tempo, Prometheus, and OpenTelemetry Collector). The most important changes are grouped below:

OpenTelemetry instrumentation and configuration:

  • Added OpenTelemetry dependencies (Npgsql.OpenTelemetry, OpenTelemetry.Exporter.OpenTelemetryProtocol, OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, OpenTelemetry.Instrumentation.Http, OpenTelemetry.Instrumentation.Runtime) to both Directory.Packages.props and SnakeAid.Api.csproj for tracing and metrics support. [1] [2]
  • Implemented AddOpenTelemetryServices extension in DependencyInjection.cs, configuring tracing (ASP.NET Core, HTTP, Npgsql) and metrics, with OTLP exporter endpoint configurable via appsettings.
  • Registered OpenTelemetry services in the application startup (Program.cs).
  • Added OpenTelemetry configuration section to appsettings.Example.json for endpoint setup.

Observability stack and configuration:

  • Added a new docker-compose.grafana.yml file to run the full observability stack (Grafana, Loki, Tempo, Prometheus, OpenTelemetry Collector, and the API itself) for local development and monitoring.
  • Introduced configuration files for each observability component:
    • observability/grafana-datasources.yaml for Grafana data sources (Loki, Tempo, Prometheus)
    • observability/loki-config.yaml for Loki log storage
    • observability/tempo-config.yaml for Tempo tracing backend
    • observability/prometheus.yaml for Prometheus scraping OTEL Collector metrics
    • observability/otel-collector-config.yaml for OpenTelemetry Collector pipelines

Logging enhancements:

  • Added Serilog.Sinks.Grafana.Loki to the logging configuration in appsettings.Example.json and provided a sample Loki sink configuration for structured log shipping. [1] [2]

Documentation:

  • Updated the SnakeAid.Docs submodule to the latest commit, which may include related documentation for observability.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added comprehensive observability stack with tracing, metrics, and log collection capabilities.
    • Integrated Grafana dashboards for monitoring application performance and traces.
    • Added Docker Compose configuration for running local observability environment.
  • Chores

    • Added observability dependencies and configuration files.

@coderabbitai

coderabbitai Bot commented Feb 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request adds OpenTelemetry observability infrastructure to the SnakeAid.Api application. It introduces tracing and metrics collection through OpenTelemetry, configures exporters for Loki, Tempo, and Prometheus, and provides a complete Docker Compose stack with Grafana for visualization and monitoring.

Changes

Cohort / File(s) Summary
Package Configuration
Directory.Packages.props, SnakeAid.Api/SnakeAid.Api.csproj
Added six OpenTelemetry NuGet packages (Exporter.OpenTelemetryProtocol, Extensions.Hosting, Instrumentation.AspNetCore, Instrumentation.Http, Instrumentation.Runtime, and Npgsql.OpenTelemetry) along with their versions.
Dependency Injection & Startup
SnakeAid.Api/DI/DependencyInjection.cs, SnakeAid.Api/Program.cs
Introduced AddOpenTelemetryServices() extension method to configure ASP.NET Core, HTTP, and Npgsql instrumentation with OTLP export; registered the new service in application startup.
Application Configuration
SnakeAid.Api/appsettings.Example.json
Added Serilog Grafana Loki sink configuration with batching settings and OpenTelemetry endpoint configuration pointing to the OTLP collector.
Docker & Observability Stack
docker-compose.grafana.yml, observability/*
Created multi-service Docker Compose setup with API, Loki, Tempo, Prometheus, Grafana, and OpenTelemetry Collector; includes Grafana datasource configuration, and configuration files for Loki, Tempo, OpenTelemetry Collector, and Prometheus.
Environment & Submodule
.gitignore, SnakeAid.Docs
Added .env to gitignore under Environment variables section; updated documentation submodule reference.

Sequence Diagram

sequenceDiagram
    participant API as SnakeAid API
    participant Instrumentation as OpenTelemetry<br/>Instrumentation
    participant OTLPCollector as OTLP Collector
    participant Tempo as Tempo
    participant Loki as Loki
    participant Prometheus as Prometheus
    participant Grafana as Grafana

    API->>Instrumentation: Emit traces & metrics<br/>(ASP.NET, HTTP, Npgsql)
    Instrumentation->>OTLPCollector: Send via OTLP<br/>(gRPC/HTTP)
    
    OTLPCollector->>Tempo: Export traces
    OTLPCollector->>Prometheus: Export metrics<br/>(scrape endpoint)
    
    Note over API: Logs via Serilog<br/>GrafanaLoki sink
    API->>Loki: Send structured logs
    
    Grafana->>Tempo: Query traces
    Grafana->>Loki: Query logs
    Grafana->>Prometheus: Query metrics
    Grafana->>Grafana: Visualize &<br/>correlate data
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hops with glee through logs and traces,
Metrics bloom in all the places,
Tempo keeps the spans so bright,
While Grafana shows the light,
Observability's our delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (37 files):

⚔️ .gitignore (content)
⚔️ .vscode/launch.json (content)
⚔️ Directory.Packages.props (content)
⚔️ Jenkinsfile (content)
⚔️ SnakeAid.Api/Controllers/RescueMissionController.cs (content)
⚔️ SnakeAid.Api/Controllers/SnakebiteIncidentController.cs (content)
⚔️ SnakeAid.Api/DI/DependencyInjection.cs (content)
⚔️ SnakeAid.Api/Program.cs (content)
⚔️ SnakeAid.Api/Properties/launchSettings.json (content)
⚔️ SnakeAid.Api/SnakeAid.Api.csproj (content)
⚔️ SnakeAid.Api/appsettings.Example.json (content)
⚔️ SnakeAid.Core/Domains/RescueRequestSession.cs (content)
⚔️ SnakeAid.Core/Domains/SnakeCatchingMission.cs (content)
⚔️ SnakeAid.Core/Domains/SnakeCatchingRequest.cs (content)
⚔️ SnakeAid.Core/Domains/SnakeSpecies.cs (content)
⚔️ SnakeAid.Core/Domains/SnakebiteIncident.cs (content)
⚔️ SnakeAid.Core/Mappings/MapsterConfig.cs (content)
⚔️ SnakeAid.Core/Mappings/SnakebiteIncidentMapper.cs (content)
⚔️ SnakeAid.Core/Middlewares/ApiExceptionHandlerMiddleware.cs (content)
⚔️ SnakeAid.Core/Responses/SnakebiteIncident/CreateIncidentResponse.cs (content)
⚔️ SnakeAid.Repository/Data/Configurations/CatchingMissionDetailConfiguration.cs (content)
⚔️ SnakeAid.Repository/Data/Configurations/CatchingRequestDetailConfiguration.cs (content)
⚔️ SnakeAid.Repository/Data/Configurations/RescueMissionConfiguration.cs (content)
⚔️ SnakeAid.Repository/Data/Configurations/SnakebiteIncidentConfiguration.cs (content)
⚔️ SnakeAid.Repository/Implements/GenericRepository.cs (content)
⚔️ SnakeAid.Repository/Implements/UnitOfWork.cs (content)
⚔️ SnakeAid.Repository/Interfaces/IUnitOfWork.cs (content)
⚔️ SnakeAid.Repository/Migrations/SnakeAidDbContextModelSnapshot.cs (content)
⚔️ SnakeAid.Repository/Seeds/DataSeeder.cs (content)
⚔️ SnakeAid.Service/Implements/FirstAidGuidelineService.cs (content)
⚔️ SnakeAid.Service/Implements/RescueMissionService.cs (content)
⚔️ SnakeAid.Service/Implements/RescueRequestSessionService.cs (content)
⚔️ SnakeAid.Service/Implements/SnakeAIService.cs (content)
⚔️ SnakeAid.Service/Implements/SnakebiteIncidentService.cs (content)
⚔️ SnakeAid.Service/Interfaces/IRescueRequestSessionService.cs (content)
⚔️ SnakeAid.Service/Interfaces/ISnakebiteIncidentService.cs (content)
⚔️ docker-compose.yml (content)

These conflicts must be resolved before merging into dev.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references the main change—adding an observability stack with OpenTelemetry, Grafana, Loki, Tempo, and Prometheus—which aligns with the substantive work in the PR. However, the title contains a spelling error ('intergration' instead of 'integration') and unnecessary punctuation/formatting issues that detract from clarity.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devops/OPS004-grafana_otel_serilog
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch devops/OPS004-grafana_otel_serilog
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@docker-compose.grafana.yml`:
- Around line 62-64: The compose file currently enables anonymous admin access
via GF_AUTH_ANONYMOUS_ENABLED=true, GF_AUTH_ANONYMOUS_ORG_ROLE=Admin and
disables login with GF_AUTH_DISABLE_LOGIN_FORM=true; change these for safety by
setting GF_AUTH_ANONYMOUS_ENABLED=false (or removing the env var), set
GF_AUTH_ANONYMOUS_ORG_ROLE to a least-privilege role like Viewer if anonymous
must remain, and enable the login form (GF_AUTH_DISABLE_LOGIN_FORM=false);
additionally move any dev-only anonymous settings into a
docker-compose.override.yml or an .env.dev file and ensure production
deployments do not include these vars.

In `@observability/grafana-datasources.yaml`:
- Around line 3-37: Tempo's tracesToLogs/tracesToMetrics fail because Prometheus
and Loki datasources lack explicit uids; add uid: 'Prometheus' to the Prometheus
datasource block and uid: 'Loki' to the Loki datasource block so the
tracesToMetrics.datasourceUid and tracesToLogs.datasourceUid resolve, and also
remove the irrelevant graphiteVersion entry from Prometheus.jsonData (the keys
to edit are the Prometheus and Loki datasource definitions and the jsonData
object under Prometheus).

In `@observability/loki-config.yaml`:
- Around line 30-31: The ruler configuration includes alertmanager_url pointing
to localhost which doesn't match our Docker Compose services; either remove the
entire ruler block (the "ruler:" stanza) from loki-config.yaml if you don't
intend to use Alertmanager, or update alertmanager_url to point to the
Alertmanager service name used in Compose (e.g., change alertmanager_url to
"http://alertmanager:9093") and add a matching Alertmanager service to Docker
Compose; ensure the symbol names "ruler" and "alertmanager_url" are updated
consistently with the Compose service name.
- Around line 20-28: The current schema_config only contains a boltdb-shipper
entry with schema v11 which is deprecated in Loki 3.x; to migrate safely, keep
the existing schema_config block exactly as-is (the boltdb-shipper / schema: v11
entry) and add a second schema_config entry with store: tsdb, schema: v13 and a
future from date (e.g., 2026-03-01) so new data uses TSDB while old queries
still work; also add tsdb_shipper settings in storage_config (configure
active_index_directory and cache_location) and ensure the compactor is
configured to run with TSDB for proper retention and query performance (avoid
setting the TSDB from date earlier than needed).

In `@observability/tempo-config.yaml`:
- Around line 24-25: The overrides block enables metrics_generator_processors
(service-graphs, span-metrics) but there is no metrics_generator configuration,
so add a metrics_generator block that defines at minimum
metrics_generator.storage.path for the WAL directory and
metrics_generator.storage.remote_write pointing to a Prometheus-compatible
remote write endpoint (e.g., http://prometheus:9090/api/v1/write); ensure the
new metrics_generator block is present alongside overrides so the service-graphs
and span-metrics processors can function correctly.

In `@SnakeAid.Api/appsettings.Example.json`:
- Around line 40-59: The example config's GrafanaLoki requestUri ("requestUri"
inside the "GrafanaLoki" entry) uses the Docker-only hostname http://loki:3100
which will fail for local (non-container) development; update the example to use
http://localhost:3100 as the default requestUri or add a clear inline comment
above the "GrafanaLoki" block explaining that http://loki:3100 is valid only
inside Docker Compose and developers running the API locally should use
http://localhost:3100 (refer to the "GrafanaLoki" name and its "Args.requestUri"
field).

In `@SnakeAid.Api/DI/DependencyInjection.cs`:
- Around line 289-321: The metrics builder in AddOpenTelemetryServices is
missing a ResourceBuilder so exported metrics won't have service.name; update
the metrics configuration (metricsProviderBuilder) to use the same
ResourceBuilder used for tracing (extract
ResourceBuilder.CreateDefault().AddService("SnakeAid.Api") into a local
variable) and apply it to the metrics provider just like
tracerProviderBuilder.SetResourceBuilder(...), ensuring the AddOtlpExporter call
on metricsProviderBuilder sends metrics with the service resource attached.

In `@SnakeAid.Api/Program.cs`:
- Line 108: Add validation around the OpenTelemetry endpoint inside the
AddOpenTelemetryServices implementation: check the configured string (used to
create the Uri in AddOpenTelemetryServices) with Uri.TryCreate and ensure
AbsoluteUri (or catch FormatException) and, if invalid, throw an
ArgumentException or log an explicit error describing the misconfigured
OpenTelemetry:Endpoint instead of letting new Uri(...) throw; update the code
path that currently does new Uri(otlpEndpoint) to use TryCreate and a clear
failure message indicating the invalid URI and the expected format.
🧹 Nitpick comments (7)
SnakeAid.Api/appsettings.Example.json (1)

70-72: Same Docker-internal hostname concern for OpenTelemetry endpoint.

http://otel-collector:4317 only resolves inside Docker. The DependencyInjection.cs has a localhost:4317 fallback, so this is partially mitigated, but the example config should note this is for the Docker Compose setup.

observability/otel-collector-config.yaml (1)

19-28: Consider adding a debug exporter for collector troubleshooting.

When debugging the observability stack locally, having a debug (formerly logging) exporter in the service pipelines can help diagnose data flow issues. This is optional for a dev-only stack.

♻️ Example addition
 exporters:
   otlp:
     endpoint: tempo:4317
     tls:
       insecure: true
   prometheus:
     endpoint: "0.0.0.0:8889"
     namespace: "snakeaid_api"
+  debug:
+    verbosity: basic
 
 service:
   pipelines:
     traces:
       receivers: [otlp]
       processors: [batch]
-      exporters: [otlp] # To Tempo (via OTLP)
+      exporters: [otlp, debug] # To Tempo (via OTLP)
     metrics:
       receivers: [otlp]
       processors: [batch]
       exporters: [prometheus] # Expose via Prometheus scrape endpoint
SnakeAid.Api/DI/DependencyInjection.cs (2)

294-294: Malformed OpenTelemetry:Endpoint config will throw an unhandled UriFormatException at startup.

If someone misconfigures the endpoint value, new Uri(otlpEndpoint) on lines 307/315 will throw with no clear indication of which setting is wrong. A guard or early validation with a descriptive error message would improve the developer experience.

🛡️ Proposed validation
 var otlpEndpoint = configuration["OpenTelemetry:Endpoint"] ?? "http://localhost:4317";
+
+if (!Uri.TryCreate(otlpEndpoint, UriKind.Absolute, out var otlpUri))
+{
+    throw new InvalidOperationException(
+        $"Invalid OpenTelemetry:Endpoint URI: '{otlpEndpoint}'. Expected an absolute URI (e.g. http://localhost:4317).");
+}

Then use otlpUri in both exporter configs instead of new Uri(otlpEndpoint).


300-300: Hardcoded service name — consider reading it from configuration or assembly metadata.

"SnakeAid.Api" is repeated in AddSource, AddService, and AddMeter. If the service name ever changes or you want per-environment naming, a single constant or config value would be cleaner.

docker-compose.grafana.yml (3)

2-16: snakeaid-api has no depends_on for otel-collector — early traces/metrics may be lost.

The API will start sending OTLP data immediately, but if the collector isn't ready yet, those initial spans and metrics will be dropped. Consider adding a dependency:

🔧 Proposed fix
   snakeaid-api:
     image: thekhiem7/snakeaid-api:latest
     pull_policy: always
     build:
       context: .
       dockerfile: Dockerfile
     ports:
       - "8080:8080"
     environment:
       - DOPPLER_TOKEN=${DOPPLER_TOKEN}
       - DOPPLER_CONFIG=snake-aid/dev
     restart: unless-stopped
+    depends_on:
+      - otel-collector
     networks:
       - snakeaid-net

31-33: Bind-mounting ./observability/tempo-data will create a local directory with Tempo's data — make sure it's gitignored.

This directory will accumulate trace data locally. If it's not in .gitignore, it could accidentally get committed.

#!/bin/bash
# Check if tempo-data is gitignored
rg -n 'tempo-data' .gitignore || echo "tempo-data is NOT in .gitignore"

19-19: All images use :latest tags — builds won't be reproducible.

Pinning image versions (e.g., grafana/loki:2.9.4) ensures the stack behaves consistently across environments and avoids surprise breakage from upstream changes. This is especially relevant for the otel-collector, which has had breaking config changes between versions.

Also applies to: 29-29, 44-44, 56-56, 69-69

Comment on lines +62 to +64
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_DISABLE_LOGIN_FORM=true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Grafana is wide-open with anonymous Admin access — acceptable for local dev only.

Anonymous access with Admin role and login disabled means anyone with network access has full Grafana admin rights. This is fine for a local dev stack but ensure this compose file is never used in staging/production or exposed publicly (e.g., via a cloud VM with open ports).

🤖 Prompt for AI Agents
In `@docker-compose.grafana.yml` around lines 62 - 64, The compose file currently
enables anonymous admin access via GF_AUTH_ANONYMOUS_ENABLED=true,
GF_AUTH_ANONYMOUS_ORG_ROLE=Admin and disables login with
GF_AUTH_DISABLE_LOGIN_FORM=true; change these for safety by setting
GF_AUTH_ANONYMOUS_ENABLED=false (or removing the env var), set
GF_AUTH_ANONYMOUS_ORG_ROLE to a least-privilege role like Viewer if anonymous
must remain, and enable the login form (GF_AUTH_DISABLE_LOGIN_FORM=false);
additionally move any dev-only anonymous settings into a
docker-compose.override.yml or an .env.dev file and ensure production
deployments do not include these vars.

Comment on lines +3 to +37
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: false
jsonData:
graphiteVersion: '1.1'

- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true

- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
jsonData:
tracesToLogs:
datasourceUid: 'Loki'
tags: ['job', 'instance', 'pod', 'namespace']
mappedTags: [{ key: 'service.name', value: 'app' }]
mapTagNamesEnabled: false
spanStartTimeShift: '0'
spanEndTimeShift: '0'
filterByTraceID: true
filterBySpanID: true
tracesToMetrics:
datasourceUid: 'Prometheus'
tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }]
queries:
- name: 'Sample query'
query: 'sum(rate(traces_spanmetrics_latency_bucket{$$__tags}[5m]))'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Tempo's datasourceUid references won't resolve — datasource UIDs are not explicitly set.

Lines 24 and 33 reference datasourceUid: 'Loki' and datasourceUid: 'Prometheus', but neither the Loki nor Prometheus datasource definitions include an explicit uid field. Grafana auto-generates UIDs (they won't be "Loki" or "Prometheus"), so the traces-to-logs and traces-to-metrics links will silently fail.

🔧 Proposed fix — add explicit UIDs
   - name: Prometheus
     type: prometheus
     access: proxy
+    uid: Prometheus
     url: http://prometheus:9090
     isDefault: false
-    jsonData:
-      graphiteVersion: '1.1'

   - name: Loki
     type: loki
     access: proxy
+    uid: Loki
     url: http://loki:3100
     isDefault: true

Also: the graphiteVersion under Prometheus jsonData is not relevant for a Prometheus-type datasource and can be removed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: false
jsonData:
graphiteVersion: '1.1'
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
jsonData:
tracesToLogs:
datasourceUid: 'Loki'
tags: ['job', 'instance', 'pod', 'namespace']
mappedTags: [{ key: 'service.name', value: 'app' }]
mapTagNamesEnabled: false
spanStartTimeShift: '0'
spanEndTimeShift: '0'
filterByTraceID: true
filterBySpanID: true
tracesToMetrics:
datasourceUid: 'Prometheus'
tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }]
queries:
- name: 'Sample query'
query: 'sum(rate(traces_spanmetrics_latency_bucket{$$__tags}[5m]))'
datasources:
- name: Prometheus
type: prometheus
access: proxy
uid: Prometheus
url: http://prometheus:9090
isDefault: false
- name: Loki
type: loki
access: proxy
uid: Loki
url: http://loki:3100
isDefault: true
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
jsonData:
tracesToLogs:
datasourceUid: 'Loki'
tags: ['job', 'instance', 'pod', 'namespace']
mappedTags: [{ key: 'service.name', value: 'app' }]
mapTagNamesEnabled: false
spanStartTimeShift: '0'
spanEndTimeShift: '0'
filterByTraceID: true
filterBySpanID: true
tracesToMetrics:
datasourceUid: 'Prometheus'
tags: [{ key: 'service.name', value: 'service' }, { key: 'job' }]
queries:
- name: 'Sample query'
query: 'sum(rate(traces_spanmetrics_latency_bucket{$$__tags}[5m]))'
🤖 Prompt for AI Agents
In `@observability/grafana-datasources.yaml` around lines 3 - 37, Tempo's
tracesToLogs/tracesToMetrics fail because Prometheus and Loki datasources lack
explicit uids; add uid: 'Prometheus' to the Prometheus datasource block and uid:
'Loki' to the Loki datasource block so the tracesToMetrics.datasourceUid and
tracesToLogs.datasourceUid resolve, and also remove the irrelevant
graphiteVersion entry from Prometheus.jsonData (the keys to edit are the
Prometheus and Loki datasource definitions and the jsonData object under
Prometheus).

Comment on lines +20 to +28
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Loki 3.0 boltdb-shipper deprecated TSDB schema v13 migration

💡 Result:

In Loki 3.x, BoltDB / boltdb-shipper is deprecated, and Grafana recommends migrating indexing to TSDB (current development focus). Boltdb-shipper “may be removed in future major versions of Loki.” [1][2]

Recommended migration approach (boltdb-shipper → TSDB, schema v13)

1) Keep your existing boltdb-shipper period(s) unchanged (for old data)

Do not modify historical schema_config entries (store/schema/prefix/period). Those must remain as-is if you want to keep querying old logs. [2][3][4]

2) Add a new future-dated period_config that switches to TSDB + v13

Add another entry to schema_config.configs with:

  • from: a date/time in the future (relative to when you deploy the config)
  • store: tsdb
  • schema: v13 (recommended in the TSDB migration docs)
  • same object_store (unless you intentionally change it)

Example (adapt to your object store, prefixes, and dates): [1]

schema_config:
  configs:
    - from: 2023-01-01
      store: boltdb-shipper
      object_store: s3   # or gcs/filesystem/etc
      schema: v11        # or your current schema
      index:
        prefix: index_
        period: 24h

    - from: 2026-03-01   # MUST be in the future when rolled out
      store: tsdb
      object_store: s3
      schema: v13
      index:
        prefix: index_
        period: 24h

Common failure mode: if the TSDB from date is wrong (too early), Loki will try to read older data using the TSDB rules and you’ll “lose” old logs in queries until you fix the date. [3]

3) Configure tsdb_shipper in storage_config

At minimum configure:

  • active_index_directory
  • cache_location [1]

Example: [1]

storage_config:
  tsdb_shipper:
    active_index_directory: /data/tsdb-index
    cache_location: /data/tsdb-cache

4) Run the compactor (strongly recommended for TSDB)

Grafana strongly recommends running the compactor with TSDB for compaction/retention and better query performance. [1]

Notes specific to “schema v13”

  • The migration docs explicitly show TSDB with schema: v13 as the target schema. [1]
  • Ensure your old period keeps its original schema (for example v11/v12), otherwise you can break querying of historical data. [4]

If you share your current schema_config + storage_config (and object store: S3/GCS/Azure/filesystem), I can point out the exact minimal diff for a safe cutover date.


boltdb-shipper with schema v11 is deprecated in Loki 3.x; migrate to TSDB with schema v13.

Do not replace the existing boltdb-shipper entry directly—this breaks queries on historical data. Instead:

  1. Keep the current boltdb-shipper entry unchanged (required for querying old logs)
  2. Add a new schema_config entry with store: tsdb, schema: v13, and a future from date (e.g., 2026-03-01)
  3. Configure tsdb_shipper in storage_config with active_index_directory and cache_location
  4. Consider running the compactor with TSDB for better retention and query performance

Setting the TSDB from date incorrectly (too early) will prevent queries on historical data until corrected.

🤖 Prompt for AI Agents
In `@observability/loki-config.yaml` around lines 20 - 28, The current
schema_config only contains a boltdb-shipper entry with schema v11 which is
deprecated in Loki 3.x; to migrate safely, keep the existing schema_config block
exactly as-is (the boltdb-shipper / schema: v11 entry) and add a second
schema_config entry with store: tsdb, schema: v13 and a future from date (e.g.,
2026-03-01) so new data uses TSDB while old queries still work; also add
tsdb_shipper settings in storage_config (configure active_index_directory and
cache_location) and ensure the compactor is configured to run with TSDB for
proper retention and query performance (avoid setting the TSDB from date earlier
than needed).

Comment on lines +30 to +31
ruler:
alertmanager_url: http://localhost:9093

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

alertmanager_url references a non-existent service.

http://localhost:9093 points to an Alertmanager that isn't part of the Docker Compose stack. This will cause ruler errors if alerting rules are configured. Either remove the ruler block or add an Alertmanager service.

🤖 Prompt for AI Agents
In `@observability/loki-config.yaml` around lines 30 - 31, The ruler configuration
includes alertmanager_url pointing to localhost which doesn't match our Docker
Compose services; either remove the entire ruler block (the "ruler:" stanza)
from loki-config.yaml if you don't intend to use Alertmanager, or update
alertmanager_url to point to the Alertmanager service name used in Compose
(e.g., change alertmanager_url to "http://alertmanager:9093") and add a matching
Alertmanager service to Docker Compose; ensure the symbol names "ruler" and
"alertmanager_url" are updated consistently with the Compose service name.

Comment on lines +24 to +25
overrides:
metrics_generator_processors: [service-graphs, span-metrics]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

❓ Verification inconclusive

Grafana Tempo metrics_generator configuration remote_write requirements


Tempo’s metrics-generator remote_write is essentially a Prometheus Agent that uses a WAL and then remote-writes generated metrics to a Prometheus-compatible endpoint. The practical requirements are:

  • You must set a local WAL directory: metrics_generator.storage.path (Tempo docs indicate the WAL path “must be set” / is required for this storage block). [1][2]
  • You must configure at least one remote_write endpoint under metrics_generator.storage.remote_write, using standard Prometheus remote_write config (at minimum, a url, typically pointing to a Prometheus-compatible /api/v1/write). [1][2]
  • The remote_write receiver must accept what you send:
    • If you enable exemplars (send_exemplars: true), the backend must support ingesting exemplars. Example configs show this option commonly enabled. [2]
    • If you enable native histograms, the receiving metrics backend must be configured to ingest them. [3]
  • Multi-tenancy behavior:
    • By default, the metrics-generator forwards X-Scope-OrgID to the remote_write endpoint (can be disabled with remote_write_add_org_id_header: false). [1][3]
    • If you need per-tenant auth headers, configure remote_write_headers via overrides (requires Tempo 2.4.0+). [4]

Minimal example (illustrative, from Grafana Tempo docs):

metrics_generator:
  storage:
    path: /tmp/tempo/generator/wal
    remote_write:
      - url: http://prometheus:9090/api/v1/write
        send_exemplars: true

[2]

Sources:
[1] Grafana Tempo configuration (metrics-generator storage/remote_write) https://grafana.com/docs/tempo/latest/configuration/
[2] Grafana Tempo “Deploy on Linux” example config (metrics_generator.storage.path + remote_write.url) https://grafana.com/docs/tempo/latest/set-up-for-tracing/setup-tempo/deploy/locally/linux/
[3] Metrics-generator docs (remote write + native histograms note) https://grafana.com/docs/tempo/latest/metrics-from-traces/metrics-generator/
[4] Metrics-generator multitenancy support (remote_write_headers, Tempo >=2.4.0) https://grafana.com/docs/tempo/latest/metrics-from-traces/metrics-generator/multitenancy/


Add metrics_generator configuration block to enable metrics processors.

The metrics_generator_processors override enables service-graphs and span-metrics generation, but without a corresponding metrics_generator block, these processors won't actually function. Tempo requires at minimum:

  • metrics_generator.storage.path: WAL directory for metrics buffering
  • metrics_generator.storage.remote_write: Prometheus-compatible remote write endpoint (e.g., http://prometheus:9090/api/v1/write)

Without these, the processor setting is ineffective.

🤖 Prompt for AI Agents
In `@observability/tempo-config.yaml` around lines 24 - 25, The overrides block
enables metrics_generator_processors (service-graphs, span-metrics) but there is
no metrics_generator configuration, so add a metrics_generator block that
defines at minimum metrics_generator.storage.path for the WAL directory and
metrics_generator.storage.remote_write pointing to a Prometheus-compatible
remote write endpoint (e.g., http://prometheus:9090/api/v1/write); ensure the
new metrics_generator block is present alongside overrides so the service-graphs
and span-metrics processors can function correctly.

Comment on lines +40 to +59
{
"Name": "GrafanaLoki",
"Args": {
"requestUri": "http://loki:3100",
"labels": [
{
"key": "app",
"value": "SnakeAid.API"
},
{
"key": "environment",
"value": "Development"
}
],
"propertiesAsLabels": ["Application", "Environment"],
"queueLimit": 10000,
"batchPostingLimit": 1000,
"period": "00:00:02"
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Serilog.Sinks.Grafana.Loki requestUri endpoint configuration format

💡 Result:

In Serilog.Sinks.Grafana.Loki the configured uri/Url is the root/base address of the Loki server (scheme + host + optional port). The sink then posts to Loki’s push endpoint POST /loki/api/v1/push. [1] [2]

Example (appsettings.json)

{
  "Serilog": {
    "Using": [ "Serilog.Sinks.Grafana.Loki" ],
    "WriteTo": [
      {
        "Name": "GrafanaLoki",
        "Args": {
          "uri": "http://localhost:3100"
        }
      }
    ]
  }
}

uri is documented as “the root URI of Loki”. [1] (and the README shows the same format). [2]

The actual HTTP endpoint Loki expects

Loki’s ingest endpoint is:

  • POST /loki/api/v1/push [3]

So with uri = http://localhost:3100, the sink will send to:

  • http://localhost:3100/loki/api/v1/push (conceptually: base + /loki/api/v1/push) [3]

Note if you’re sending to Grafana Agent / Alloy “loki_push_api”

Some agent configs use /api/v1/push (without the /loki prefix). If you point the sink at an agent endpoint that only accepts /api/v1/push, you can get 404s because the sink targets /loki/api/v1/push. [4]

Sources:
[1] Serilog.Sinks.Grafana.Loki wiki “Application settings” (parameter uri = root URI of Loki) (github.com)
[2] serilog-contrib/serilog-sinks-grafana-loki README (examples use "uri": "http://localhost:3100") (github.com)
[3] Grafana Loki HTTP API docs (ingest endpoint POST /loki/api/v1/push) (grafana.com)
[4] Issue report describing /api/v1/push vs /loki/api/v1/push mismatch with Grafana Agent (lightrun.com)

Citations:


🏁 Script executed:

# Search for configuration files and patterns in the codebase
find . -name "appsettings*.json" -o -name "*.Example.json" | head -20

Repository: Snake-AID/SnakeAid.Backend

Length of output: 109


🏁 Script executed:

# Look for Serilog configuration or documentation in the repo
rg -i "loki|grafana" --type json -A 3 -B 3 | head -100

Repository: Snake-AID/SnakeAid.Backend

Length of output: 52


🏁 Script executed:

# Search for any comments or documentation about local development setup
rg -i "docker|localhost|environment" SnakeAid.Api/ | head -50

Repository: Snake-AID/SnakeAid.Backend

Length of output: 2955


🏁 Script executed:

# Search for all appsettings files
fd "appsettings" SnakeAid.Api/

Repository: Snake-AID/SnakeAid.Backend

Length of output: 52


🏁 Script executed:

# Check how appsettings are loaded in Program.cs
rg -A 10 "appsettings|Configuration" SnakeAid.Api/Program.cs | head -60

Repository: Snake-AID/SnakeAid.Backend

Length of output: 2379


Loki requestUri uses Docker-internal hostname — won't work for local development outside containers.

The URI http://loki:3100 only resolves within the Docker network. Developers running the API locally will get connection failures. Since this is an example file, consider using http://localhost:3100 as the default, or add a comment noting this configuration is for Docker Compose environments only.

🤖 Prompt for AI Agents
In `@SnakeAid.Api/appsettings.Example.json` around lines 40 - 59, The example
config's GrafanaLoki requestUri ("requestUri" inside the "GrafanaLoki" entry)
uses the Docker-only hostname http://loki:3100 which will fail for local
(non-container) development; update the example to use http://localhost:3100 as
the default requestUri or add a clear inline comment above the "GrafanaLoki"
block explaining that http://loki:3100 is valid only inside Docker Compose and
developers running the API locally should use http://localhost:3100 (refer to
the "GrafanaLoki" name and its "Args.requestUri" field).

Comment on lines +289 to +321

#region OpenTelemetry

public static IServiceCollection AddOpenTelemetryServices(this IServiceCollection services, IConfiguration configuration)
{
var otlpEndpoint = configuration["OpenTelemetry:Endpoint"] ?? "http://localhost:4317"; // fallback for local without docker

services.AddOpenTelemetry()
.WithTracing(tracerProviderBuilder =>
{
tracerProviderBuilder
.AddSource("SnakeAid.Api")
.SetResourceBuilder(
ResourceBuilder.CreateDefault()
.AddService("SnakeAid.Api"))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddNpgsql()
.AddOtlpExporter(opts => opts.Endpoint = new Uri(otlpEndpoint));
})
.WithMetrics(metricsProviderBuilder =>
{
metricsProviderBuilder
.AddMeter("SnakeAid.Api")
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter(opts => opts.Endpoint = new Uri(otlpEndpoint));
});

return services;
}

#endregion

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Metrics are missing ResourceBuilder — service name won't be attached to exported metrics.

Tracing configures .SetResourceBuilder(…).AddService("SnakeAid.Api") but the metrics builder does not. This means metrics exported via OTLP will lack the service.name resource attribute, making it harder to filter/correlate in Prometheus/Grafana.

🔧 Proposed fix
         .WithMetrics(metricsProviderBuilder =>
         {
             metricsProviderBuilder
                 .AddMeter("SnakeAid.Api")
+                .SetResourceBuilder(
+                    ResourceBuilder.CreateDefault()
+                        .AddService("SnakeAid.Api"))
                 .AddAspNetCoreInstrumentation()
                 .AddRuntimeInstrumentation()
                 .AddOtlpExporter(opts => opts.Endpoint = new Uri(otlpEndpoint));
         });

Consider also extracting the shared ResourceBuilder into a local variable to avoid duplication:

var resourceBuilder = ResourceBuilder.CreateDefault().AddService("SnakeAid.Api");
🤖 Prompt for AI Agents
In `@SnakeAid.Api/DI/DependencyInjection.cs` around lines 289 - 321, The metrics
builder in AddOpenTelemetryServices is missing a ResourceBuilder so exported
metrics won't have service.name; update the metrics configuration
(metricsProviderBuilder) to use the same ResourceBuilder used for tracing
(extract ResourceBuilder.CreateDefault().AddService("SnakeAid.Api") into a local
variable) and apply it to the metrics provider just like
tracerProviderBuilder.SetResourceBuilder(...), ensuring the AddOtlpExporter call
on metricsProviderBuilder sends metrics with the service resource attached.

Comment thread SnakeAid.Api/Program.cs
builder.Services.AddScoped<SnakeAid.Service.Implements.Email.EmailTemplateService>();

builder.Services.AddServices(builder.Configuration);
builder.Services.AddOpenTelemetryServices(builder.Configuration);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.cs" | xargs grep -l "AddOpenTelemetryServices" | head -20

Repository: Snake-AID/SnakeAid.Backend

Length of output: 136


🏁 Script executed:

find . -type f -name "*DependencyInjection*" -o -name "*OpenTelemetry*" | head -20

Repository: Snake-AID/SnakeAid.Backend

Length of output: 110


🏁 Script executed:

rg "AddOpenTelemetryServices" -A 30

Repository: Snake-AID/SnakeAid.Backend

Length of output: 4821


The AddOpenTelemetryServices implementation handles missing configuration well but doesn't validate invalid URIs.

The method correctly uses a null-coalescing operator to default to http://localhost:4317 when OpenTelemetry:Endpoint is missing. However, if the configuration contains an invalid URI string (e.g., malformed value), the new Uri(otlpEndpoint) call will throw a FormatException at startup. Consider adding URI validation or error handling to fail gracefully with a clear error message when the endpoint is misconfigured.

🤖 Prompt for AI Agents
In `@SnakeAid.Api/Program.cs` at line 108, Add validation around the OpenTelemetry
endpoint inside the AddOpenTelemetryServices implementation: check the
configured string (used to create the Uri in AddOpenTelemetryServices) with
Uri.TryCreate and ensure AbsoluteUri (or catch FormatException) and, if invalid,
throw an ArgumentException or log an explicit error describing the misconfigured
OpenTelemetry:Endpoint instead of letting new Uri(...) throw; update the code
path that currently does new Uri(otlpEndpoint) to use TryCreate and a clear
failure message indicating the invalid URI and the expected format.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant