From b389b4190258264d35a22eaedbbb7fbb282103b6 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Wed, 11 Feb 2026 03:12:15 +0700 Subject: [PATCH 1/6] docs: update SnakeAid.Docs submodule --- SnakeAid.Docs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SnakeAid.Docs b/SnakeAid.Docs index 1976c6f4..13a83208 160000 --- a/SnakeAid.Docs +++ b/SnakeAid.Docs @@ -1 +1 @@ -Subproject commit 1976c6f4e921976550de5fba46f33197db52923c +Subproject commit 13a83208ec40201b8f5ab8cae171ef7e4bd87be7 From b245db0896a31fa5811f92582ca4e51d838f9476 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Wed, 11 Feb 2026 21:24:55 +0700 Subject: [PATCH 2/6] Ignore local environment files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 16768962..7d8e1442 100644 --- a/.gitignore +++ b/.gitignore @@ -362,6 +362,9 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd +# Environment variables +.env + # appsettings.json appsettings.json From 908ba63e4f43779e301e9546fabc201e6b38663f Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Wed, 11 Feb 2026 21:24:55 +0700 Subject: [PATCH 3/6] Add OpenTelemetry package versions and project references --- Directory.Packages.props | 6 ++++++ SnakeAid.Api/SnakeAid.Api.csproj | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6d8c3c25..4c3e4ef6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,6 +25,12 @@ + + + + + + diff --git a/SnakeAid.Api/SnakeAid.Api.csproj b/SnakeAid.Api/SnakeAid.Api.csproj index 63f99af7..2b30b8b5 100644 --- a/SnakeAid.Api/SnakeAid.Api.csproj +++ b/SnakeAid.Api/SnakeAid.Api.csproj @@ -11,7 +11,13 @@ - + + + + + + + From 7a9a0361f9388ab0a9912aca3f0e5fe65426c0dc Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Wed, 11 Feb 2026 21:24:55 +0700 Subject: [PATCH 4/6] Register OpenTelemetry tracing and metrics and add default configuration --- SnakeAid.Api/DI/DependencyInjection.cs | 38 ++++++++++++++++++++++++++ SnakeAid.Api/Program.cs | 1 + SnakeAid.Api/appsettings.Example.json | 3 ++ 3 files changed, 42 insertions(+) diff --git a/SnakeAid.Api/DI/DependencyInjection.cs b/SnakeAid.Api/DI/DependencyInjection.cs index dd6045fa..9fd1b7a0 100644 --- a/SnakeAid.Api/DI/DependencyInjection.cs +++ b/SnakeAid.Api/DI/DependencyInjection.cs @@ -15,6 +15,11 @@ using Serilog; using Doppler.Extensions.Configuration; // Ensure this is available +using OpenTelemetry.Trace; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Exporter; +using Npgsql; namespace SnakeAid.Api.DI; @@ -281,4 +286,37 @@ public static IServiceCollection AddSwagger(this IServiceCollection services) } #endregion + + #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 } diff --git a/SnakeAid.Api/Program.cs b/SnakeAid.Api/Program.cs index d76b4fec..a5a5ce48 100644 --- a/SnakeAid.Api/Program.cs +++ b/SnakeAid.Api/Program.cs @@ -105,6 +105,7 @@ public static async Task Main(string[] args) builder.Services.AddScoped(); builder.Services.AddServices(builder.Configuration); + builder.Services.AddOpenTelemetryServices(builder.Configuration); // Register services using Scrutor builder.Services.Scan(scan => scan diff --git a/SnakeAid.Api/appsettings.Example.json b/SnakeAid.Api/appsettings.Example.json index 7ba7a771..fbda5b41 100644 --- a/SnakeAid.Api/appsettings.Example.json +++ b/SnakeAid.Api/appsettings.Example.json @@ -46,6 +46,9 @@ } ] }, + "OpenTelemetry": { + "Endpoint": "http://otel-collector:4317" + }, "Jwt": { "SecretKey": "mysuperlongsecretkeythatishard2guess123!", "RefreshSecretKey": "anotherultrasecretkeythatmustbe32chars!!", From 78cc638b2300f48acd67b9174c7c9f9d96de3a18 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Wed, 11 Feb 2026 21:24:55 +0700 Subject: [PATCH 5/6] Configure Serilog to send logs to Grafana Loki --- SnakeAid.Api/appsettings.Example.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/SnakeAid.Api/appsettings.Example.json b/SnakeAid.Api/appsettings.Example.json index fbda5b41..4f2ad573 100644 --- a/SnakeAid.Api/appsettings.Example.json +++ b/SnakeAid.Api/appsettings.Example.json @@ -10,6 +10,7 @@ "Serilog", "Serilog.Sinks.Console", "Serilog.Sinks.SQLite", + "Serilog.Sinks.Grafana.Loki", "Serilog.Enrichers.Environment", "Serilog.Enrichers.Span" ], @@ -36,6 +37,26 @@ "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Application} {Message:lj} {Properties:j}{NewLine}{Exception}" } }, + { + "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" + } + }, { "Name": "SQLite", "Args": { From 1cd2c090aeb6a996adead15feb2da6d362f7909b Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Wed, 11 Feb 2026 21:24:56 +0700 Subject: [PATCH 6/6] Add observability Docker Compose and configuration files (Grafana, Loki, Tempo, Prometheus, OTEL collector) --- docker-compose.grafana.yml | 90 ++++++++++++++++++++++++ observability/grafana-datasources.yaml | 37 ++++++++++ observability/loki-config.yaml | 31 ++++++++ observability/otel-collector-config.yaml | 28 ++++++++ observability/prometheus.yaml | 8 +++ observability/tempo-config.yaml | 25 +++++++ 6 files changed, 219 insertions(+) create mode 100644 docker-compose.grafana.yml create mode 100644 observability/grafana-datasources.yaml create mode 100644 observability/loki-config.yaml create mode 100644 observability/otel-collector-config.yaml create mode 100644 observability/prometheus.yaml create mode 100644 observability/tempo-config.yaml diff --git a/docker-compose.grafana.yml b/docker-compose.grafana.yml new file mode 100644 index 00000000..51e66d78 --- /dev/null +++ b/docker-compose.grafana.yml @@ -0,0 +1,90 @@ +services: + 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 + networks: + - snakeaid-net + + loki: + image: grafana/loki:latest + command: -config.file=/etc/loki/local-config.yaml + ports: + - "3100:3100" + volumes: + - ./observability/loki-config.yaml:/etc/loki/local-config.yaml + networks: + - snakeaid-net + + tempo: + image: grafana/tempo:latest + command: [ "-config.file=/etc/tempo.yaml" ] + volumes: + - ./observability/tempo-config.yaml:/etc/tempo.yaml + - ./observability/tempo-data:/var/tempo + ports: + - "14268" # jaeger ingest + - "3200" # tempo + - "4317" # otlp grpc + - "4318" # otlp http + - "9411" # zipkin + networks: + - snakeaid-net + + prometheus: + image: prom/prometheus:latest + command: + - --config.file=/etc/prometheus/prometheus.yml + - --web.enable-lifecycle + volumes: + - ./observability/prometheus.yaml:/etc/prometheus/prometheus.yml + ports: + - "9090:9090" + networks: + - snakeaid-net + + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + volumes: + - ./observability/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + networks: + - snakeaid-net + + otel-collector: + image: otel/opentelemetry-collector:latest + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./observability/otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "1888:1888" # pprof extension + - "8888:8888" # Prometheus metrics exposed by the collector + - "8889:8889" # Prometheus exporter metrics + - "13133:13133" # health_check extension + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP http receiver + - "55679:55679" # zpages extension + depends_on: + - loki + - tempo + - prometheus + networks: + - snakeaid-net + +networks: + snakeaid-net: + driver: bridge \ No newline at end of file diff --git a/observability/grafana-datasources.yaml b/observability/grafana-datasources.yaml new file mode 100644 index 00000000..5c47aebb --- /dev/null +++ b/observability/grafana-datasources.yaml @@ -0,0 +1,37 @@ +apiVersion: 1 + +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]))' diff --git a/observability/loki-config.yaml b/observability/loki-config.yaml new file mode 100644 index 00000000..290e7d4d --- /dev/null +++ b/observability/loki-config.yaml @@ -0,0 +1,31 @@ +auth_enabled: false + +limits_config: + allow_structured_metadata: false + +server: + http_listen_port: 3100 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2020-10-24 + store: boltdb-shipper + object_store: filesystem + schema: v11 + index: + prefix: index_ + period: 24h + +ruler: + alertmanager_url: http://localhost:9093 diff --git a/observability/otel-collector-config.yaml b/observability/otel-collector-config.yaml new file mode 100644 index 00000000..9ad27472 --- /dev/null +++ b/observability/otel-collector-config.yaml @@ -0,0 +1,28 @@ +receivers: + otlp: + protocols: + grpc: + http: + +processors: + batch: + +exporters: + otlp: + endpoint: tempo:4317 + tls: + insecure: true + prometheus: + endpoint: "0.0.0.0:8889" + namespace: "snakeaid_api" + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp] # To Tempo (via OTLP) + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] # Expose via Prometheus scrape endpoint diff --git a/observability/prometheus.yaml b/observability/prometheus.yaml new file mode 100644 index 00000000..36300dc8 --- /dev/null +++ b/observability/prometheus.yaml @@ -0,0 +1,8 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'otel-collector' + scrape_interval: 10s + static_configs: + - targets: ['otel-collector:8889'] diff --git a/observability/tempo-config.yaml b/observability/tempo-config.yaml new file mode 100644 index 00000000..8219e237 --- /dev/null +++ b/observability/tempo-config.yaml @@ -0,0 +1,25 @@ +server: + http_listen_port: 3200 + +distributor: + receivers: + otlp: + protocols: + grpc: + http: + +ingester: + trace_idle_period: 10s + max_block_bytes: 1_000_000 + max_block_duration: 5m + +storage: + trace: + backend: local + wal: + path: /var/tempo/wal + local: + path: /var/tempo/blocks + +overrides: + metrics_generator_processors: [service-graphs, span-metrics]