--- name: loki-logging description: Configure Grafana Loki for log aggregation and analysis. category: devops risk: critical source: https://github.com/BagelHole/DevOps-Security-Agent-Skills source_repo: BagelHole/DevOps-Security-Agent-Skills source_type: community date_added: '2026-09-20' license: MIT license_source: https://github.com/BagelHole/DevOps-Security-Agent-Skills/blob/main/LICENSE compatibility: Requires the relevant platform CLIs (kubectl, helm, terraform, git, CI runners) and authorized access to the target environment. Docs-only; helper scripts and templates not bundled. metadata: author: devops-skills version: '1.0' --- # Grafana Loki Aggregate and query logs with Grafana Loki, the Prometheus-inspired logging system. ## When to Use This Skill Use this skill when: - Implementing cost-effective log aggregation - Building logging for Kubernetes environments - Integrating logs with Grafana dashboards - Querying logs with label-based filtering - Preferring lighter-weight alternative to ELK Promtail reached EOL on March 2, 2026; use [Grafana Alloy or another supported client](https://grafana.com/docs/loki/latest/send-data/promtail/) for new deployments. The 2.9/schema v11/loki-stack examples below are historical and must be migrated with the installed Loki/chart version, not pasted into current production. Retention for boltdb-shipper needs the appropriate compactor setup; table_manager settings alone do not prove expiry. Keep auth-disabled Loki loopback/private behind an authenticated gateway and restrict sensitive host log access. Persist shipper positions to avoid replay on container recreation. Docker discovery requires approved socket access, absent from the Compose fragment; do not grant it blindly. line_length must be explicitly extracted/computed before unwrap, parse errors need filtering and zero-denominator rates need a defined no-data policy. Validate alert delivery and resource limits before raising ingestion quotas. ## Prerequisites - Docker or Kubernetes - Grafana for visualization - Promtail or other log shipper ## Architecture Overview ``` ┌─────────────┐ ┌──────────┐ ┌──────────┐ │ Application │────▶│ Promtail │────▶│ Loki │ └─────────────┘ └──────────┘ └──────────┘ │ ▼ ┌──────────┐ │ Grafana │ └──────────┘ ``` ## Docker Deployment ```yaml # docker-compose.yml version: '3.8' services: loki: image: grafana/loki:2.9.0 ports: - "127.0.0.1:3100:3100" volumes: - ./loki-config.yaml:/etc/loki/local-config.yaml - loki-data:/loki command: -config.file=/etc/loki/local-config.yaml promtail: image: grafana/promtail:2.9.0 volumes: - ./promtail-config.yaml:/etc/promtail/config.yaml - /var/log:/var/log:ro - /var/lib/docker/containers:/var/lib/docker/containers:ro command: -config.file=/etc/promtail/config.yaml grafana: image: grafana/grafana:10.2.0 ports: - "127.0.0.1:3000:3000" volumes: - grafana-data:/var/lib/grafana - ./grafana/provisioning:/etc/grafana/provisioning environment: - GF_AUTH_ANONYMOUS_ENABLED=false - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?Set a unique Grafana password} volumes: loki-data: grafana-data: ``` ## Loki Configuration ```yaml # loki-config.yaml auth_enabled: 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 storage_config: boltdb_shipper: active_index_directory: /loki/index cache_location: /loki/cache shared_store: filesystem limits_config: reject_old_samples: true reject_old_samples_max_age: 168h max_query_series: 5000 max_query_parallelism: 2 chunk_store_config: max_look_back_period: 168h table_manager: retention_deletes_enabled: true retention_period: 168h ``` ## Promtail Configuration ```yaml # promtail-config.yaml server: http_listen_port: 9080 grpc_listen_port: 0 positions: filename: /tmp/positions.yaml clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: # System logs - job_name: system static_configs: - targets: - localhost labels: job: varlogs __path__: /var/log/*.log # Docker container logs - job_name: docker docker_sd_configs: - host: unix:///var/run/docker.sock refresh_interval: 5s relabel_configs: - source_labels: ['__meta_docker_container_name'] regex: '/(.*)' target_label: 'container' - source_labels: ['__meta_docker_container_log_stream'] target_label: 'stream' # Application logs with parsing - job_name: application static_configs: - targets: - localhost labels: job: application __path__: /var/log/app/*.log pipeline_stages: - json: expressions: level: level message: message timestamp: timestamp - labels: level: - timestamp: source: timestamp format: RFC3339 ``` ## Kubernetes Deployment ```bash # Using Helm helm repo add grafana https://grafana.github.io/helm-charts helm install loki grafana/loki-stack \ --namespace monitoring \ --create-namespace \ --set grafana.enabled=true \ --set promtail.enabled=true ``` ### Promtail DaemonSet ```yaml apiVersion: apps/v1 kind: DaemonSet metadata: name: promtail namespace: monitoring spec: selector: matchLabels: app: promtail template: metadata: labels: app: promtail spec: containers: - name: promtail image: grafana/promtail:2.9.0 args: - -config.file=/etc/promtail/promtail.yaml volumeMounts: - name: config mountPath: /etc/promtail - name: varlog mountPath: /var/log - name: varlibdockercontainers mountPath: /var/lib/docker/containers readOnly: true volumes: - name: config configMap: name: promtail-config - name: varlog hostPath: path: /var/log - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers ``` ## LogQL Queries ### Basic Queries ```logql # All logs from a job {job="application"} # Filter by label {job="application", level="error"} # Multiple labels {namespace="production", container="api"} # Regex match {job=~"app.*"} ``` ### Log Pipeline ```logql # Filter by content {job="application"} |= "error" # Exclude content {job="application"} != "debug" # Regex filter {job="application"} |~ "user_id=[0-9]+" # JSON parsing {job="application"} | json | level="error" # Line format {job="application"} | json | line_format "{{.level}}: {{.message}}" ``` ### Metric Queries ```logql # Count logs in the five-minute window (use rate for per-second) count_over_time({job="application"}[5m]) # Rate of errors rate({job="application", level="error"}[5m]) # Sum by label sum by (level) (count_over_time({job="application"}[5m])) # Top services by error count topk(5, sum by (service) (count_over_time({level="error"}[1h]))) ``` ### Aggregations ```logql # Average log line length avg_over_time({job="application"} | unwrap line_length [5m]) # Percentile of numeric field quantile_over_time(0.95, {job="application"} | json | unwrap response_time [5m]) # Error percentage sum(rate({job="application", level="error"}[5m])) / sum(rate({job="application"}[5m])) * 100 ``` ## Pipeline Stages ```yaml # promtail-config.yaml pipeline_stages: # Parse JSON logs - json: expressions: level: level message: msg trace_id: trace_id # Extract with regex - regex: expression: 'user_id=(?P\d+)' # Add labels from parsed fields - labels: level: # Keep user_id as parsed content/structured metadata, not an indexed label. # Modify timestamp - timestamp: source: timestamp format: '2006-01-02T15:04:05.000Z' # Filter logs - match: selector: '{level="debug"}' action: drop # Add static labels - static_labels: environment: production # Modify log line - template: source: message template: '{{ ToUpper .Value }}' ``` ## Grafana Integration ### Data Source Configuration ```yaml # grafana/provisioning/datasources/loki.yaml apiVersion: 1 datasources: - name: Loki type: loki access: proxy url: http://loki:3100 isDefault: false jsonData: maxLines: 1000 ``` ### Dashboard Panel ```json { "title": "Application Logs", "type": "logs", "datasource": "Loki", "targets": [ { "expr": "{job=\"application\"} | json", "refId": "A" } ], "options": { "showTime": true, "showLabels": true, "wrapLogMessage": true } } ``` ## Recording Rules ```yaml # loki-rules.yaml groups: - name: error_rates interval: 1m rules: - record: job:log_errors:rate5m expr: | sum by (job) (rate({level="error"}[5m])) ``` ## Alerting ```yaml # loki-alerts.yaml groups: - name: log_alerts rules: - alert: HighErrorRate expr: | sum(rate({level="error"}[5m])) > 10 for: 5m labels: severity: critical annotations: summary: "High error rate in logs" description: "Error rate is {{ $value }} errors/second" ``` ## Common Issues ### Issue: High Memory Usage **Problem**: Loki consuming too much memory **Solution**: Reduce max_query_series, limit query time range ### Issue: Logs Not Appearing **Problem**: Promtail not shipping logs **Solution**: Check positions file, verify file paths, check label configuration ### Issue: Query Timeout **Problem**: LogQL queries timing out **Solution**: Add more specific label filters, reduce time range ### Issue: Ingestion Rate Limit **Problem**: Logs being dropped **Solution**: Increase per_stream_rate_limit in limits_config ## Best Practices - Use meaningful labels (avoid high cardinality) - Filter by labels before log content - Parse logs at collection time with Promtail - Set appropriate retention periods - Use recording rules for common queries - Implement proper multitenancy for large deployments - Monitor Loki's own metrics - Use chunk caching for better performance ## Related Skills - prometheus-grafana (`prometheus-grafana`) - Metrics monitoring - elk-stack (`elk-stack`) - Alternative logging - alerting-oncall (`alerting-oncall`) - Alert management ## Limitations - Guidance executes against real environments: confirm target, blast radius, and rollback plan before applying anything. - Never deploy to production without explicit approval. Docs-only import: upstream scripts and templates not bundled. ### Example ```bash git status && git diff --stat kubectl diff -f manifest.yaml ``` > Adapted from [BagelHole/DevOps-Security-Agent-Skills](https://github.com/BagelHole/DevOps-Security-Agent-Skills) (MIT); frontmatter, When to Use/Limitations, and safety boundaries added for upstream compliance. Docs-only import: helper scripts and templates not bundled.