diff --git a/01_standalone_grafana/README.md b/01_standalone_grafana/README.md new file mode 100644 index 0000000..5ceafc6 --- /dev/null +++ b/01_standalone_grafana/README.md @@ -0,0 +1,30 @@ +Example 1 — Grafana by itself +=== + +No additional resources are needed to run this example. +Execute the following command to start Grafana: + +```bash +docker run -i -t --rm -p 3000:3000 grafana/grafana:12.0.2 +``` + +You can then access Grafana at `http://localhost:3000` with the default credentials (username: `admin`, password: `admin`). + +##### Custom login credentials + +```bash +docker run -i -t --rm -p 3000:3000 \ +-e GF_SECURITY_ADMIN_USER=a \ +-e GF_SECURITY_ADMIN_PASSWORD=a \ +grafana/grafana:12.0.2 +``` + +##### Custom login credentials and preinstalled plugin + +```bash +docker run -i -t --rm -p 3000:3000 \ +-e GF_SECURITY_ADMIN_USER=a \ +-e GF_SECURITY_ADMIN_PASSWORD=a \ +-e "GF_INSTALL_PLUGINS=grafana-sentry-datasource" \ +grafana/grafana:12.0.2 +``` \ No newline at end of file diff --git a/02_metrics_with_prometheus/README.md b/02_metrics_with_prometheus/README.md new file mode 100644 index 0000000..fa413a3 --- /dev/null +++ b/02_metrics_with_prometheus/README.md @@ -0,0 +1,6 @@ +Example 2 — Grafana with Prometheus (metrics data) +=== + +```bash +docker-compose up +``` diff --git a/02_metrics_with_prometheus/docker-compose.yml b/02_metrics_with_prometheus/docker-compose.yml new file mode 100644 index 0000000..603e2be --- /dev/null +++ b/02_metrics_with_prometheus/docker-compose.yml @@ -0,0 +1,56 @@ +services: + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--storage.tsdb.retention.time=200h' + - '--web.enable-lifecycle' + networks: + - grafana_network + + grafana: + image: grafana/grafana:12.0.2 + container_name: grafana + ports: + - "3000:3000" + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + depends_on: + - prometheus + networks: + - grafana_network + + # Example microservice with metrics endpoint + metrics_generator: + image: prom/node-exporter:latest + container_name: example_metrics + ports: + - "9100:9100" + networks: + - grafana_network + +volumes: + grafana_data: + driver: local + prometheus_data: + driver: local + +networks: + grafana_network: + driver: bridge + diff --git a/02_metrics_with_prometheus/grafana/provisioning/dashboards/dashboards.yml b/02_metrics_with_prometheus/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..673349d --- /dev/null +++ b/02_metrics_with_prometheus/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,10 @@ +apiVersion: 1 +providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/02_metrics_with_prometheus/grafana/provisioning/dashboards/example-dashboard.json b/02_metrics_with_prometheus/grafana/provisioning/dashboards/example-dashboard.json new file mode 100644 index 0000000..0952986 --- /dev/null +++ b/02_metrics_with_prometheus/grafana/provisioning/dashboards/example-dashboard.json @@ -0,0 +1,418 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "stepBefore", + "lineWidth": 4, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "go_memstats_heap_objects", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Heap objects", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 17, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "node_load1", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "node_load5", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "B", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "node_load15", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "C", + "useBackend": false + } + ], + "title": "Load avg", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 1, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "go_goroutines", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Go routines", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "{__name__=\"node_nfs_packets_total\", instance=\"metrics_generator:9100\", job=\"metrics_generator\", protocol=\"tcp\"}" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [] + } + ] + }, + "gridPos": { + "h": 9, + "w": 4, + "x": 8, + "y": 8 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "process_open_fds", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Open file descriptors", + "type": "stat" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Example dashboard with system metrics", + "uid": "2e48b630-2206-4876-b606-2132a21d1934", + "version": 5 +} \ No newline at end of file diff --git a/02_metrics_with_prometheus/grafana/provisioning/datasources/prometheus.yml b/02_metrics_with_prometheus/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 0000000..8049912 --- /dev/null +++ b/02_metrics_with_prometheus/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true \ No newline at end of file diff --git a/02_metrics_with_prometheus/prometheus/prometheus.yml b/02_metrics_with_prometheus/prometheus/prometheus.yml new file mode 100644 index 0000000..91851d6 --- /dev/null +++ b/02_metrics_with_prometheus/prometheus/prometheus.yml @@ -0,0 +1,14 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: +# - job_name: 'prometheus' +# static_configs: +# - targets: ['localhost:9090'] + + - job_name: 'metrics_generator' + static_configs: + - targets: ['metrics_generator:9100'] + scrape_interval: 2s + scrape_timeout: 1s \ No newline at end of file diff --git a/03_logs_with_loki/README.md b/03_logs_with_loki/README.md new file mode 100644 index 0000000..f2b5eb5 --- /dev/null +++ b/03_logs_with_loki/README.md @@ -0,0 +1,6 @@ +Example 3 — Grafana with Loki (logs data) +=== + +```bash +docker-compose up +``` diff --git a/03_logs_with_loki/docker-compose.yml b/03_logs_with_loki/docker-compose.yml new file mode 100644 index 0000000..3363a1b --- /dev/null +++ b/03_logs_with_loki/docker-compose.yml @@ -0,0 +1,55 @@ +services: + + grafana: + image: grafana/grafana:12.0.2 + container_name: grafana + ports: + - "3000:3000" + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + depends_on: + - loki + networks: + - grafana_network + + loki: + image: grafana/loki:latest + container_name: loki + ports: + - "3100:3100" + volumes: + - ./loki:/etc/loki + - loki_data:/loki + command: -config.file=/etc/loki/loki.yml + networks: + - grafana_network + + + log-generator: + build: + context: ./log-generator + dockerfile: Dockerfile + container_name: log-generator + networks: + - grafana_network + depends_on: + - loki + +volumes: + grafana_data: + driver: local + prometheus_data: + driver: local + loki_data: + driver: local + +networks: + grafana_network: + driver: bridge + diff --git a/03_logs_with_loki/grafana/provisioning/dashboards/dashboards.yml b/03_logs_with_loki/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..673349d --- /dev/null +++ b/03_logs_with_loki/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,10 @@ +apiVersion: 1 +providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/03_logs_with_loki/grafana/provisioning/dashboards/example-dashboard.json b/03_logs_with_loki/grafana/provisioning/dashboards/example-dashboard.json new file mode 100644 index 0000000..a995dcf --- /dev/null +++ b/03_logs_with_loki/grafana/provisioning/dashboards/example-dashboard.json @@ -0,0 +1,274 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 29, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "direction": "backward", + "editorMode": "code", + "expr": "sum by(service_name) (rate({job=\"log-generator\"} [$__auto]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "Logs per service", + "type": "piechart" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 20, + "x": 4, + "y": 0 + }, + "id": 1, + "options": { + "cellHeight": "md", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "direction": "backward", + "editorMode": "builder", + "expr": "{severity=\"error\"}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Errors", + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 4, + "y": 13 + }, + "id": 2, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "direction": "backward", + "editorMode": "builder", + "expr": "sum by(severity) (rate({severity=\"critical\"} [$__auto]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "Error log rate", + "type": "gauge" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 16, + "x": 8, + "y": 13 + }, + "id": 4, + "options": { + "dedupStrategy": "none", + "enableInfiniteScrolling": false, + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "pluginVersion": "12.0.2", + "targets": [ + { + "direction": "backward", + "editorMode": "builder", + "expr": "{job=\"log-generator\"}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Log tail", + "transparent": true, + "type": "logs" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Simple logs dashboard", + "uid": "a86bbd08-0669-4d22-9c03-f9eacfa00788", + "version": 6 +} \ No newline at end of file diff --git a/03_logs_with_loki/grafana/provisioning/datasources/loki.yml b/03_logs_with_loki/grafana/provisioning/datasources/loki.yml new file mode 100644 index 0000000..b913263 --- /dev/null +++ b/03_logs_with_loki/grafana/provisioning/datasources/loki.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + isDefault: false \ No newline at end of file diff --git a/03_logs_with_loki/log-generator/Dockerfile b/03_logs_with_loki/log-generator/Dockerfile new file mode 100644 index 0000000..b57b646 --- /dev/null +++ b/03_logs_with_loki/log-generator/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:alpine AS builder + +ADD logger.go /logger.go + +RUN go build -o /service /logger.go + +FROM scratch + +COPY --from=builder /service . + +ENTRYPOINT [ "/service" ] diff --git a/03_logs_with_loki/log-generator/logger.go b/03_logs_with_loki/log-generator/logger.go new file mode 100644 index 0000000..af14918 --- /dev/null +++ b/03_logs_with_loki/log-generator/logger.go @@ -0,0 +1,76 @@ +// Copyright Quesma, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "math/rand" + "net/http" + "time" +) + +const url = "http://loki:3100/loki/api/v1/push" + +func main() { + hostNames := []string{"zeus", "cassandra", "hercules", + "oracle", "athena", "jupiter", "poseidon", "hades", "artemis", "apollo", "demeter", + "dionysus", "hephaestus", "hermes", "hestia", "iris", "nemesis", "pan", "persephone", "prometheus", "selen"} + + serviceNames := []string{"frontend", "backend", "database", "cache", "queue", "monitoring", "loadbalancer", "proxy", + "storage", "auth", "api", "web", "worker", "scheduler", "cron", "admin", "service", "gateway", "service", "service", "service"} + + sourceNames := []string{"kubernetes", "ubuntu", "debian", "centos", "redhat", "fedora", "arch", "gentoo", "alpine", "suse", + "rhel", "coreos", "docker", "rancher", "vmware", "xen", "hyperv", "openstack", "aws", "gcp", "azure", "digitalocean"} + + severityNames := []string{"info", "info", "info", "info", "info", "info", "warning", "error", "critical", "debug", "debug", "debug"} + + messageNames := []string{"User logged in", "User logged out", "User created", "User deleted", "User updated", + "User password changed", "User password reset", "User password reset requested", "User password reset failed"} + + for { + time.Sleep(time.Duration(1000+rand.Intn(2000)) * time.Millisecond) + + timestamp := time.Now() + severity := severityNames[rand.Intn(len(severityNames))] + source := sourceNames[rand.Intn(len(sourceNames))] + serviceName := serviceNames[rand.Intn(len(serviceNames))] + hostName := hostNames[rand.Intn(len(hostNames))] + message := messageNames[rand.Intn(len(messageNames))] + + // Create Loki push request format + logEntry := map[string]interface{}{ + "streams": []map[string]interface{}{ + { + "stream": map[string]string{ + "severity": severity, + "source": source, + "service_name": serviceName, + "host_name": hostName, + "job": "log-generator", + }, + "values": [][]string{ + { + fmt.Sprintf("%d", timestamp.UnixNano()), + message, + }, + }, + }, + }, + } + + body, err := json.Marshal(logEntry) + if err != nil { + log.Fatal(err) + } + + resp, err := http.Post(url, "application/json", bytes.NewBuffer(body)) + if err != nil { + log.Fatal(err) + } + + resp.Body.Close() + } +} diff --git a/03_logs_with_loki/loki/loki.yml b/03_logs_with_loki/loki/loki.yml new file mode 100644 index 0000000..aef3904 --- /dev/null +++ b/03_logs_with_loki/loki/loki.yml @@ -0,0 +1,42 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + +ingester: + lifecycler: + address: 127.0.0.1 + ring: + kvstore: + store: inmemory + replication_factor: 1 + final_sleep: 0s + chunk_idle_period: 5m + chunk_retain_period: 30s + wal: + dir: /loki/wal + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + tsdb_shipper: + active_index_directory: /loki/tsdb-shipper-active + cache_location: /loki/tsdb-shipper-cache + filesystem: + directory: /loki/chunks + +limits_config: + reject_old_samples: true + reject_old_samples_max_age: 168h + +compactor: + working_directory: /loki/tsdb-shipper-compactor + diff --git a/04_traces_with_tempo/README.md b/04_traces_with_tempo/README.md new file mode 100644 index 0000000..0c77735 --- /dev/null +++ b/04_traces_with_tempo/README.md @@ -0,0 +1,6 @@ +Example 4 — Grafana with Tempo (distributed tracing data) +=== + +```bash +docker-compose up +``` diff --git a/04_traces_with_tempo/docker-compose.yml b/04_traces_with_tempo/docker-compose.yml new file mode 100644 index 0000000..9ab174e --- /dev/null +++ b/04_traces_with_tempo/docker-compose.yml @@ -0,0 +1,56 @@ +services: + + grafana: + image: grafana/grafana:12.0.2 + ports: + - "3000:3000" + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + depends_on: + - tempo + networks: + - grafana_network + + tempo: + image: grafana/tempo:latest + command: ["-config.file=/etc/tempo.yaml"] + volumes: + - ./tempo/tempo.yml:/etc/tempo.yaml + - ./tempo/data:/var/tempo + ports: + - "14268:14268" # jaeger ingest + - "3200:3200" # tempo + - "9095:9095" # tempo grpc + - "4317:4317" # otlp grpc + - "4318:4318" # otlp http + - "9411:9411" # zipkin + networks: + - grafana_network + + + k6-tracing: + image: ghcr.io/grafana/xk6-client-tracing:v0.0.7 + environment: + - ENDPOINT=tempo:4317 + restart: always + depends_on: + - tempo + networks: + - grafana_network + +volumes: + grafana_data: + driver: local + prometheus_data: + driver: local + +networks: + grafana_network: + driver: bridge + diff --git a/04_traces_with_tempo/grafana/provisioning/dashboards/dashboards.yml b/04_traces_with_tempo/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..673349d --- /dev/null +++ b/04_traces_with_tempo/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,10 @@ +apiVersion: 1 +providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/04_traces_with_tempo/grafana/provisioning/datasources/tempo.yml b/04_traces_with_tempo/grafana/provisioning/datasources/tempo.yml new file mode 100644 index 0000000..54e6fa1 --- /dev/null +++ b/04_traces_with_tempo/grafana/provisioning/datasources/tempo.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Tempo + type: tempo + access: proxy + url: http://tempo:3200 + isDefault: false \ No newline at end of file diff --git a/04_traces_with_tempo/tempo/tempo.yml b/04_traces_with_tempo/tempo/tempo.yml new file mode 100644 index 0000000..b9a8c2f --- /dev/null +++ b/04_traces_with_tempo/tempo/tempo.yml @@ -0,0 +1,91 @@ +stream_over_http_enabled: true +server: + http_listen_port: 3200 + log_level: info + +cache: + background: + writeback_goroutines: 5 + +query_frontend: + search: + duration_slo: 5s + throughput_bytes_slo: 1.073741824e+09 + metadata_slo: + duration_slo: 5s + throughput_bytes_slo: 1.073741824e+09 + trace_by_id: + duration_slo: 100ms + metrics: + max_duration: 200h # maximum duration of a metrics query, increase for local setups + query_backend_after: 5m + duration_slo: 5s + throughput_bytes_slo: 1.073741824e+09 + +distributor: + usage: + cost_attribution: + enabled: true + receivers: # this configuration will listen on all ports and protocols that tempo is capable of. + jaeger: # the receives all come from the OpenTelemetry collector. more configuration information can + protocols: # be found there: https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver + thrift_http: # + endpoint: "tempo:14268" # for a production deployment you should only enable the receivers you need! + grpc: + endpoint: "tempo:14250" + thrift_binary: + endpoint: "tempo:6832" + thrift_compact: + endpoint: "tempo:6831" + zipkin: + endpoint: "tempo:9411" + otlp: + protocols: + grpc: + endpoint: "tempo:4317" + http: + endpoint: "tempo:4318" + opencensus: + endpoint: "tempo:55678" + +ingester: + max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally + +compactor: + compaction: + block_retention: 720h # overall Tempo trace retention. set for demo purposes + +metrics_generator: + registry: + external_labels: + source: tempo + cluster: docker-compose + storage: + path: /var/tempo/generator/wal + remote_write: + - url: http://prometheus:9090/api/v1/write + send_exemplars: true + traces_storage: + path: /var/tempo/generator/traces + processor: + local_blocks: + filter_server_spans: false + flush_to_storage: true + +storage: + trace: + cache: "" + backend: local # backend configuration to use + wal: + path: /var/tempo/wal # where to store the wal locally + local: + path: /var/tempo/blocks + +overrides: + defaults: + cost_attribution: + dimensions: + service.name: "" + metrics_generator: + processors: [service-graphs, span-metrics, local-blocks] # enables metrics generator + generate_native_histograms: both \ No newline at end of file diff --git a/05_profiling_with_pyroscope/README.md b/05_profiling_with_pyroscope/README.md new file mode 100644 index 0000000..f09f43b --- /dev/null +++ b/05_profiling_with_pyroscope/README.md @@ -0,0 +1,6 @@ +Example 5 — Grafana with Pyroscope (profiling data) +=== + +```bash +docker-compose up +``` diff --git a/05_profiling_with_pyroscope/docker-compose.yml b/05_profiling_with_pyroscope/docker-compose.yml new file mode 100644 index 0000000..4725fa5 --- /dev/null +++ b/05_profiling_with_pyroscope/docker-compose.yml @@ -0,0 +1,44 @@ +services: + + grafana: + image: grafana/grafana:12.0.2 + ports: + - "3000:3000" + environment: + - GF_INSTALL_PLUGINS=grafana-pyroscope-app + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + depends_on: + - pyroscope + networks: + - grafana_network + + pyroscope: + image: grafana/pyroscope:latest + ports: + - 4040:4040 + networks: + - grafana_network + + sample_app: + build: + context: ./sample_app + dockerfile: Dockerfile + environment: + - PYROSCOPE_SERVER_ADDRESS=http://pyroscope:4040 + networks: + - grafana_network + +volumes: + grafana_data: + driver: local + +networks: + grafana_network: + driver: bridge + diff --git a/05_profiling_with_pyroscope/grafana/provisioning/dashboards/dashboards.yml b/05_profiling_with_pyroscope/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..673349d --- /dev/null +++ b/05_profiling_with_pyroscope/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,10 @@ +apiVersion: 1 +providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/05_profiling_with_pyroscope/sample_app/Dockerfile b/05_profiling_with_pyroscope/sample_app/Dockerfile new file mode 100644 index 0000000..24d9941 --- /dev/null +++ b/05_profiling_with_pyroscope/sample_app/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.23.11 + +WORKDIR /go/src/app + +COPY main.go go.mod go.sum ./ + +RUN go get -d ./ +RUN go build -o main . + +RUN adduser --disabled-password --gecos --quiet pyroscope +USER pyroscope + +CMD ["./main"] \ No newline at end of file diff --git a/05_profiling_with_pyroscope/sample_app/go.mod b/05_profiling_with_pyroscope/sample_app/go.mod new file mode 100644 index 0000000..71ac60a --- /dev/null +++ b/05_profiling_with_pyroscope/sample_app/go.mod @@ -0,0 +1,10 @@ +module sample_app + +go 1.23.0 + +require github.com/grafana/pyroscope-go v1.2.4 + +require ( + github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect + github.com/klauspost/compress v1.17.11 // indirect +) \ No newline at end of file diff --git a/05_profiling_with_pyroscope/sample_app/go.sum b/05_profiling_with_pyroscope/sample_app/go.sum new file mode 100644 index 0000000..5a8ab67 --- /dev/null +++ b/05_profiling_with_pyroscope/sample_app/go.sum @@ -0,0 +1,6 @@ +github.com/grafana/pyroscope-go v1.2.4 h1:B22GMXz+O0nWLatxLuaP7o7L9dvP0clLvIpmeEQQM0Q= +github.com/grafana/pyroscope-go v1.2.4/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU= +github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= +github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= diff --git a/05_profiling_with_pyroscope/sample_app/main.go b/05_profiling_with_pyroscope/sample_app/main.go new file mode 100644 index 0000000..0ec1082 --- /dev/null +++ b/05_profiling_with_pyroscope/sample_app/main.go @@ -0,0 +1,288 @@ +package main + +import ( + "context" + "crypto/rand" + "encoding/json" + "fmt" + "log" + "math" + "os" + "runtime/pprof" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/grafana/pyroscope-go" +) + +// Simulate different types of workloads + +//go:noinline +func cpuIntensiveWork(n int) { + // CPU-bound work with actual computation + sum := 0.0 + for i := 0; i < n; i++ { + sum += math.Sqrt(float64(i)) * math.Sin(float64(i)) + } + _ = sum // prevent optimization +} + +//go:noinline +func memoryIntensiveWork(size int) { + // Memory allocation and manipulation + data := make([][]byte, size) + for i := range data { + data[i] = make([]byte, 1024) + rand.Read(data[i]) + } + + // Some processing to prevent optimization + for i := range data { + for j := range data[i] { + data[i][j] = byte((int(data[i][j]) + i + j) % 256) + } + } +} + +//go:noinline +func recursiveFibonacci(n int) int { + if n <= 1 { + return n + } + return recursiveFibonacci(n-1) + recursiveFibonacci(n-2) +} + +//go:noinline +func recursiveFactorial(n int) int { + if n <= 1 { + return 1 + } + return n * recursiveFactorial(n-1) +} + +//go:noinline +func stringProcessing(iterations int) { + var builder strings.Builder + for i := 0; i < iterations; i++ { + builder.WriteString(fmt.Sprintf("iteration-%d-", i)) + } + + // JSON marshaling/unmarshaling + data := map[string]interface{}{ + "message": builder.String(), + "count": iterations, + "nested": map[string]int{ + "a": 1, "b": 2, "c": 3, + }, + } + + jsonData, _ := json.Marshal(data) + var result map[string]interface{} + json.Unmarshal(jsonData, &result) +} + +//go:noinline +func sortingWork(size int) { + // Create random data + data := make([]int, size) + for i := range data { + data[i] = int(time.Now().UnixNano()) % 10000 + } + + // Multiple sorting algorithms + data1 := make([]int, len(data)) + copy(data1, data) + sort.Ints(data1) + + // Bubble sort for smaller datasets (inefficient by design) + if size <= 1000 { + data2 := make([]int, len(data)) + copy(data2, data) + bubbleSort(data2) + } +} + +//go:noinline +func bubbleSort(arr []int) { + n := len(arr) + for i := 0; i < n-1; i++ { + for j := 0; j < n-i-1; j++ { + if arr[j] > arr[j+1] { + arr[j], arr[j+1] = arr[j+1], arr[j] + } + } + } +} + +//go:noinline +func networkSimulation(requests int) { + // Simulate network-like delays and processing + for i := 0; i < requests; i++ { + // Simulate variable latency + latency := time.Duration(10+i%50) * time.Microsecond + time.Sleep(latency) + + // Simulate request processing + processRequest(i) + } +} + +//go:noinline +func processRequest(id int) { + // Simulate request processing with string operations + request := fmt.Sprintf("request-%d", id) + parts := strings.Split(request, "-") + if len(parts) > 1 { + num, _ := strconv.Atoi(parts[1]) + _ = num * 2 + } +} + +//go:noinline +func concurrentWork(ctx context.Context, workers int) { + var wg sync.WaitGroup + + for i := 0; i < workers; i++ { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + pyroscope.TagWrapper(ctx, pyroscope.Labels("worker", fmt.Sprintf("worker-%d", workerID)), func(c context.Context) { + // Each worker does different types of work + switch workerID % 3 { + case 0: + cpuIntensiveWork(1000000) + case 1: + memoryIntensiveWork(500) + case 2: + stringProcessing(10000) + } + }) + }(i) + } + + wg.Wait() +} + +// Different function types for varied flame graph patterns + +func fastFunction(c context.Context) { + pyroscope.TagWrapper(c, pyroscope.Labels("function", "fast", "type", "cpu"), func(c context.Context) { + cpuIntensiveWork(1000000) + }) +} + +func slowFunction(c context.Context) { + pprof.Do(c, pprof.Labels("function", "slow", "type", "mixed"), func(c context.Context) { + cpuIntensiveWork(5000000) + memoryIntensiveWork(200) + }) +} + +func recursiveFunction(c context.Context) { + pyroscope.TagWrapper(c, pyroscope.Labels("function", "recursive", "type", "fibonacci"), func(c context.Context) { + // Fibonacci creates deep call stacks + result := recursiveFibonacci(35) + _ = result + }) +} + +func memoryFunction(c context.Context) { + pyroscope.TagWrapper(c, pyroscope.Labels("function", "memory", "type", "allocation"), func(c context.Context) { + memoryIntensiveWork(1000) + }) +} + +func stringFunction(c context.Context) { + pyroscope.TagWrapper(c, pyroscope.Labels("function", "string", "type", "processing"), func(c context.Context) { + stringProcessing(50000) + }) +} + +func sortingFunction(c context.Context) { + pyroscope.TagWrapper(c, pyroscope.Labels("function", "sorting", "type", "algorithms"), func(c context.Context) { + sortingWork(5000) + }) +} + +func networkFunction(c context.Context) { + pyroscope.TagWrapper(c, pyroscope.Labels("function", "network", "type", "simulation"), func(c context.Context) { + networkSimulation(100) + }) +} + +func mathFunction(c context.Context) { + pyroscope.TagWrapper(c, pyroscope.Labels("function", "math", "type", "factorial"), func(c context.Context) { + result := recursiveFactorial(15) + _ = result + }) +} + +func concurrentFunction(c context.Context) { + pyroscope.TagWrapper(c, pyroscope.Labels("function", "concurrent", "type", "goroutines"), func(c context.Context) { + concurrentWork(c, 10) + }) +} + +func main() { + serverAddress := os.Getenv("PYROSCOPE_SERVER_ADDRESS") + if serverAddress == "" { + serverAddress = "http://localhost:4040" + } + + _, err := pyroscope.Start(pyroscope.Config{ + ApplicationName: "sample_app", + ServerAddress: serverAddress, + Logger: pyroscope.StandardLogger, + Tags: map[string]string{ + "version": "2.0", + "env": "demo", + }, + }) + if err != nil { + log.Fatalf("error starting pyroscope profiler: %v", err) + } + + // Main execution loop with varied workloads + pyroscope.TagWrapper(context.Background(), pyroscope.Labels("phase", "main"), func(c context.Context) { + iteration := 0 + for { + iteration++ + + // Create varied execution patterns + switch iteration % 9 { + case 0: + fastFunction(c) + case 1: + slowFunction(c) + case 2: + recursiveFunction(c) + case 3: + memoryFunction(c) + case 4: + stringFunction(c) + case 5: + sortingFunction(c) + case 6: + networkFunction(c) + case 7: + mathFunction(c) + case 8: + concurrentFunction(c) + } + + // Occasional heavy computation phase + if iteration%20 == 0 { + pyroscope.TagWrapper(c, pyroscope.Labels("phase", "heavy"), func(c context.Context) { + cpuIntensiveWork(10000000) + memoryIntensiveWork(2000) + }) + } + + // Short pause to make the profiling more readable + time.Sleep(10 * time.Millisecond) + } + }) +}