# Promtail config: Bunny.net CDN access logs → Loki # # Deploy on the same VPS as the APIX registry (or any host reachable from # the internet on port 5514). Bunny.net streams one TCP Syslog line per # request — including CDN cache hits — giving live telemetry that the # origin Prometheus metrics alone cannot provide. # # Setup: # 1. Install Promtail: https://grafana.com/docs/loki/latest/clients/promtail/installation/ # 2. Copy this file to /etc/promtail/cdn-logs.yaml # 3. Fill in LOKI_PUSH_URL, LOKI_USERNAME, LOKI_PASSWORD below # 4. Open TCP 5514 in the VPS firewall # 5. systemctl enable --now promtail # 6. Run setup-bunnynet.sh with SYSLOG_HOST= # # Verify stream is working: # curl -s https://api-index.org/services?capability=nlp # journalctl -u promtail -f # should show the log line within 1-2 seconds # # Grafana dashboard queries (LogQL): # Capability hit rate (live): # count_over_time({job="apix-cdn"} | label_format capability="capability" [1m]) # Cache ratio: # sum by (cache_status) (count_over_time({job="apix-cdn"}[5m])) # Zero-result detection (requires apix.search.result_count from Prometheus): # use a mixed datasource panel combining Loki counts with Prometheus summaries server: http_listen_port: 9080 grpc_listen_port: 0 log_level: info clients: - url: https://LOKI_PUSH_URL/loki/api/v1/push # e.g. logs-prod-xxx.grafana.net/loki/api/v1/push basic_auth: username: "LOKI_USERNAME" # Grafana Cloud: stack user ID (numeric) password: "LOKI_PASSWORD" # Grafana Cloud: service account token (logs:write scope) positions: filename: /tmp/promtail-cdn-positions.yaml scrape_configs: - job_name: apix-cdn-logs syslog: listen_address: 0.0.0.0:5514 label_structured_data: false labels: job: apix-cdn component: bunnynet relabel_configs: # Bunny.net includes the PoP hostname in the syslog HOSTNAME field # e.g. "pa.b-cdn.net" (Paris), "sg.b-cdn.net" (Singapore) # This is how PoPs are visible even on cache hits that never reach origin. - source_labels: [__syslog_message_hostname] target_label: cdn_pop pipeline_stages: # ── Parse Apache Combined Log Format + Bunny.net cache status suffix ── # Line: 1.2.3.x - - [12/May/2026:10:00:00 +0000] "GET /services?cap=nlp HTTP/1.1" 200 1234 "-" "Agent/1.0" HIT - regex: expression: '"(?P[A-Z]+) (?P[^" ]+) HTTP/[^"]*" (?P\d+) \d+ "[^"]*" "[^"]*" (?P\S+)$' - labels: http_method: http_status: cache_status: # HIT | MISS | BYPASS — primary cache ratio signal # ── Split path from query string ────────────────────────────────────── - regex: source: full_path expression: '^(?P[^?]+)(\?(?P.+))?$' - labels: req_path: # /services /devices /devices/{id}/replacements # ── Extract search parameters ───────────────────────────────────────── # capability is the primary analytics dimension - regex: source: query_string expression: '(?:^|&)capability=(?P[^&]*)' - labels: capability: # nlp | translation | speech-to-text | … | (empty = nav doc) - regex: source: query_string expression: '(?:^|&)deviceClass=(?P[^&]*)' - labels: device_class: # sensor | actuator | gateway | … - regex: source: query_string expression: '(?:^|&)protocol=(?P[^&]*)' - labels: protocol: # MQTT | AMQP | HTTP | … # stage only appears explicitly when non-PRODUCTION (canonical form omits it) - regex: source: query_string expression: '(?:^|&)stage=(?P[^&]*)' - labels: stage: # STAGING | DEVELOPMENT — absent means PRODUCTION # ── Drop Quarkus internal paths — never cache, no analytics value ───── - drop: source: req_path expression: '^/q/' # ── Timestamp from log line (keeps Loki time accurate) ──────────────── - timestamp: source: __syslog_message_timestamp format: RFC3339