-
-
Notifications
You must be signed in to change notification settings - Fork 0
Devops/ops004 Observation stack intergration: Serilogs with OpenTelemetry, Grafana, Loki, Tempo. Prometheus #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
b389b41
b245db0
908ba63
7a9a036
78cc638
1cd2c09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,6 +105,7 @@ public static async Task Main(string[] args) | |
| builder.Services.AddScoped<SnakeAid.Service.Implements.Email.EmailTemplateService>(); | ||
|
|
||
| builder.Services.AddServices(builder.Configuration); | ||
| builder.Services.AddOpenTelemetryServices(builder.Configuration); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "*.cs" | xargs grep -l "AddOpenTelemetryServices" | head -20Repository: Snake-AID/SnakeAid.Backend Length of output: 136 🏁 Script executed: find . -type f -name "*DependencyInjection*" -o -name "*OpenTelemetry*" | head -20Repository: Snake-AID/SnakeAid.Backend Length of output: 110 🏁 Script executed: rg "AddOpenTelemetryServices" -A 30Repository: Snake-AID/SnakeAid.Backend Length of output: 4821 The The method correctly uses a null-coalescing operator to default to 🤖 Prompt for AI Agents |
||
|
|
||
| // Register services using Scrutor | ||
| builder.Services.Scan(scan => scan | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
| } | ||
| }, | ||
|
Comment on lines
+40
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In Serilog.Sinks.Grafana.Loki the configured Example (appsettings.json){
"Serilog": {
"Using": [ "Serilog.Sinks.Grafana.Loki" ],
"WriteTo": [
{
"Name": "GrafanaLoki",
"Args": {
"uri": "http://localhost:3100"
}
}
]
}
}
The actual HTTP endpoint Loki expectsLoki’s ingest endpoint is:
So with
Note if you’re sending to Grafana Agent / Alloy “loki_push_api”Some agent configs use Sources: Citations:
🏁 Script executed: # Search for configuration files and patterns in the codebase
find . -name "appsettings*.json" -o -name "*.Example.json" | head -20Repository: 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 -100Repository: 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 -50Repository: 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 -60Repository: Snake-AID/SnakeAid.Backend Length of output: 2379 Loki The URI 🤖 Prompt for AI Agents |
||
| { | ||
| "Name": "SQLite", | ||
| "Args": { | ||
|
|
@@ -46,6 +67,9 @@ | |
| } | ||
| ] | ||
| }, | ||
| "OpenTelemetry": { | ||
| "Endpoint": "http://otel-collector:4317" | ||
| }, | ||
| "Jwt": { | ||
| "SecretKey": "mysuperlongsecretkeythatishard2guess123!", | ||
| "RefreshSecretKey": "anotherultrasecretkeythatmustbe32chars!!", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Comment on lines
+62
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Grafana is wide-open with anonymous Admin access — acceptable for local dev only. Anonymous access with 🤖 Prompt for AI Agents |
||
| 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 | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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]))' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tempo's Lines 24 and 33 reference 🔧 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: trueAlso: the 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Comment on lines
+20
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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 2) Add a new future-dated period_config that switches to TSDB + v13Add another entry to
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: 24hCommon failure mode: if the TSDB 3) Configure
|
||
|
|
||
| ruler: | ||
| alertmanager_url: http://localhost:9093 | ||
|
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| global: | ||
| scrape_interval: 15s | ||
|
|
||
| scrape_configs: | ||
| - job_name: 'otel-collector' | ||
| scrape_interval: 10s | ||
| static_configs: | ||
| - targets: ['otel-collector:8889'] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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] | ||
|
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ Verification inconclusiveGrafana 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:
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: Add The
Without these, the processor setting is ineffective. 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 theservice.nameresource 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
ResourceBuilderinto a local variable to avoid duplication:🤖 Prompt for AI Agents