2. The stack
The whole stack is one file: Grafana, Loki, Tempo, Mimir, rum-api and the ouo11y collector, with their configuration inline. Nothing is installed on your machine but containers.
:4320 Sentry bridge
+ ouo11y RUM app
Step 4. Run the ouo11y stack from one file
Section titled “Step 4. Run the ouo11y stack from one file”4a. Create the compose file
Section titled “4a. Create the compose file”Open the block below, copy all of it (the copy button is at its top right), and paste it into a new file stack/compose.yaml in VS Code. You can also download it as quickstart.yaml and save it as stack/compose.yaml.
stack/compose.yaml
# ouo quickstart: the whole local stack in one file, on the latest ouo release.# GENERATED by deploy/scripts/gen-quickstart.sh; do not edit by hand.## The ouo images default to :latest, the newest release. This file was generated for, and its# configs come from, these releases (digest = the multi-arch image index at that version):## collector 0.4.0 sha256:ca0d001dc58153671fc501cbe280a69f86dc3e681a1b4621e827dedadb7727f2 OUO_COLLECTOR_VERSION# rum-api 0.5.0 sha256:1ee9e606220df2c42f4ade4131fdd3f82085d6e814676a771754a85a6ae78336 OUO_RUM_API_VERSION# grafana 0.5.0 sha256:57607a3616c88867644be1f69abb4681ca73df05d6bf2682543418f0dfeb622f OUO_GRAFANA_VERSION (plugin ouo11y-rum-app 0.9.0)## To stay on exactly these, put e.g. OUO_GRAFANA_VERSION=0.5.0 (or 0.5.0@sha256:...) in a .env# file next to this one. Each config below comes from its image's release commit; Loki, Tempo# and Mimir from grafana-v0.5.0. A later release that needs new configs comes with a regenerated# file: fetch it again (deploy/compose/quickstart.yaml in the ouo11y/ouo11y repo).## Copy this file into an empty folder as compose.yaml, then:## echo $GHCR_TOKEN | docker login ghcr.io -u <github-user> --password-stdin # images are private for now# docker compose up -d## Grafana http://localhost:3000 admin / testing567, More apps -> ouo11y RUM# OTLP/HTTP http://localhost:4318 /v1/traces /v1/logs /v1/metrics# Replay http://localhost:4319 /v1/replay/chunks# Credential HTTP Basic, project id `demo`, project token `pt_dev_demo_0000000000000000`# (Authorization: Basic ZGVtbzpwdF9kZXZfZGVtb18wMDAwMDAwMDAwMDAwMDAw)## Ports collide? Set OUO_GRAFANA_PORT, OUO_OTLP_HTTP_PORT, OUO_REPLAY_PORT, ... in a .env# file next to this one (names and defaults: deploy/compose/.env.example in the repo). An SDK# then needs both URLs, e.g. endpoint :14318 and replayEndpoint :14319.# Stop: `docker compose down`; wipe the data too: `docker compose down -v`.## Development tokens only (they are public): fine on a laptop, never on a reachable host.# A server install is deploy/vm/install.sh. Needs Docker Compose >= 2.24 (`configs.content`).## No `name:` on purpose: the project is named after the folder, so two copies of this file in# two folders are two separate stacks (a fixed name would make `down -v` in one remove the other).
services: grafana: image: ${OUO_IMAGE_REGISTRY:-ghcr.io/ouo11y}/grafana:${OUO_GRAFANA_VERSION:-latest} ports: - "${OUO_GRAFANA_PORT:-3000}:3000" environment: GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: ${OUO_GRAFANA_ADMIN_PASSWORD:-testing567} GF_AUTH_ANONYMOUS_ENABLED: "false" GF_USERS_DEFAULT_THEME: dark GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: ouo11y-rum-app GF_SECURITY_DISABLE_FRONTEND_SANDBOX_FOR_PLUGINS: ouo11y-rum-app # Read by the plugin provisioning file; must equal rum-api's OUO_READ_TOKEN. OUO_READ_TOKEN: ${OUO_READ_TOKEN:-rt_dev_0000000000000000} # Grafana feature toggles the plugin can use (comma-separated), empty by default. OUO_FEATURE_TOGGLES_ENABLE: ${OUO_FEATURE_TOGGLES_ENABLE:-} configs: - source: grafana-datasources target: /etc/grafana/provisioning/datasources/datasources.yaml - source: grafana-plugin-app target: /etc/grafana/provisioning/plugins/ouo11y-rum-app.yaml volumes: - grafana-data:/var/lib/grafana depends_on: grafana-plugin: condition: service_completed_successfully loki: condition: service_started tempo: condition: service_started mimir: condition: service_started healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health >/dev/null 2>&1 || exit 1"] interval: 5s timeout: 3s retries: 30
# The grafana-data volume hides the plugin the image ships in /var/lib/grafana/plugins; # copy it onto the volume before every start so an image upgrade also upgrades the plugin. grafana-plugin: image: ${OUO_IMAGE_REGISTRY:-ghcr.io/ouo11y}/grafana:${OUO_GRAFANA_VERSION:-latest} entrypoint: ["sh", "-ec"] command: - | mkdir -p /pvc/plugins rm -rf /pvc/plugins/ouo11y-rum-app cp -a /var/lib/grafana/plugins/ouo11y-rum-app /pvc/plugins/ouo11y-rum-app echo "ouo11y-rum-app $$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' /pvc/plugins/ouo11y-rum-app/plugin.json | head -1) installed" volumes: - grafana-data:/pvc restart: "no"
loki: image: grafana/loki:3.7.7 command: ["-config.file=/etc/loki/config.yaml", "-config.expand-env=true"] ports: - "${OUO_LOKI_PORT:-3100}:3100" environment: OUO_RETENTION_LOGS: ${OUO_RETENTION_LOGS:-720h} OUO_LOKI_COMPACTION_INTERVAL: ${OUO_LOKI_COMPACTION_INTERVAL:-10m} OUO_LOKI_RETENTION_DELETE_DELAY: ${OUO_LOKI_RETENTION_DELETE_DELAY:-2h} OUO_LOKI_DELETE_CANCEL_PERIOD: ${OUO_LOKI_DELETE_CANCEL_PERIOD:-24h} configs: - source: loki target: /etc/loki/config.yaml - source: loki-runtime target: /etc/loki/runtime.yaml volumes: - loki-data:/loki
tempo: image: grafana/tempo:3.0.3 command: ["-config.file=/etc/tempo/config.yaml", "-config.expand-env=true"] ports: - "${OUO_TEMPO_PORT:-3200}:3200" environment: OUO_RETENTION_TRACES: ${OUO_RETENTION_TRACES:-336h} configs: - source: tempo target: /etc/tempo/config.yaml volumes: - tempo-data:/var/tempo
mimir: image: grafana/mimir:3.2.1 command: ["-config.file=/etc/mimir/config.yaml", "-config.expand-env=true"] ports: - "${OUO_MIMIR_PORT:-9009}:9009" environment: OUO_RETENTION_METRICS: ${OUO_RETENTION_METRICS:-2160h} configs: - source: mimir target: /etc/mimir/config.yaml volumes: - mimir-data:/data
# The only ingest (ADR-0007): OTLP on 4318 and replay chunks on 4319, both behind HTTP # Basic project credentials checked against the projects-htpasswd config below. otel-collector: image: ${OUO_IMAGE_REGISTRY:-ghcr.io/ouo11y}/collector:${OUO_COLLECTOR_VERSION:-latest} command: ["--config=/etc/otelcol/config.yaml"] ports: - "127.0.0.1:${OUO_OTLP_GRPC_PORT:-4317}:4317" - "${OUO_OTLP_HTTP_PORT:-4318}:4318" - "${OUO_REPLAY_PORT:-4319}:4319" - "${OUO_SENTRY_PORT:-4320}:4320" - "127.0.0.1:${OUO_COLLECTOR_HEALTH_PORT:-13133}:13133" environment: OUO_INGEST_TOKEN: ${OUO_INGEST_TOKEN:-it_dev_0000000000000000} OUO_ALLOWED_ORIGINS: ${OUO_ALLOWED_ORIGINS:-http://localhost:*,http://127.0.0.1:*} OUO_RATELIMIT_CLIENT_KEY: ${OUO_RATELIMIT_CLIENT_KEY:-address} OUO_RATELIMIT_PROJECT_RATE: ${OUO_RATELIMIT_PROJECT_RATE:-1000} OUO_RATELIMIT_PROJECT_BURST: ${OUO_RATELIMIT_PROJECT_BURST:-5000} OUO_RATELIMIT_CLIENT_RATE: ${OUO_RATELIMIT_CLIENT_RATE:-100} OUO_RATELIMIT_CLIENT_BURST: ${OUO_RATELIMIT_CLIENT_BURST:-300} OUO_SENTRY_UPSTREAM: ${OUO_SENTRY_UPSTREAM:-none} OUO_SENTRY_PROJECT_ID: ${OUO_SENTRY_PROJECT_ID:-1} OUO_SENTRY_PUBLIC_KEY: ${OUO_SENTRY_PUBLIC_KEY:-publickey0000000000000000000000} OUO_SENTRY_SERVICE_NAME: ${OUO_SENTRY_SERVICE_NAME:-sentry-demo} configs: - source: collector target: /etc/otelcol/config.yaml - source: projects-htpasswd target: /etc/otelcol/projects.htpasswd depends_on: - loki - tempo - mimir
# Internal: replay chunk sink and the plugin's API. SDKs never talk to it. rum-api: image: ${OUO_IMAGE_REGISTRY:-ghcr.io/ouo11y}/rum-api:${OUO_RUM_API_VERSION:-latest} command: ["-config.file=/etc/rum-api/config.yaml", "-config.expand-env=true"] ports: - "${OUO_RUM_API_PORT:-8080}:8080" environment: GRAFANA_URL: http://localhost:${OUO_GRAFANA_PORT:-3000} OUO_INGEST_TOKEN: ${OUO_INGEST_TOKEN:-it_dev_0000000000000000} OUO_READ_TOKEN: ${OUO_READ_TOKEN:-rt_dev_0000000000000000} # Operator API /v1/admin/* and the per-project source-map upload tokens (ADR-0011). OUO_ADMIN_TOKEN: ${OUO_ADMIN_TOKEN:-at_dev_0000000000000000} OUO_UPLOAD_TOKENS: ${OUO_UPLOAD_TOKENS:-demo:ut_dev_demo_0000000000000000} REPLAY_RETENTION: ${OUO_RETENTION_REPLAYS:-720h} LOG_RETENTION: ${OUO_RETENTION_LOGS:-720h} REPLAY_SWEEP_INTERVAL: ${OUO_REPLAY_SWEEP_INTERVAL:-1h} # The image runs as nonroot. A named volume mounted on the existing nonroot home is # seeded with that owner; one mounted on /data would be root-owned and unwritable. DATA_DIR: /home/nonroot OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318 OTEL_EXPORTER_OTLP_HEADERS: ${OUO_BACKEND_OTLP_HEADERS:-Authorization=Basic%20aW50ZXJuYWw6cHRfZGV2X2ludGVybmFsXzAwMDAwMDAwMDAwMDAw} OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf OTEL_SERVICE_NAME: rum-api OTEL_RESOURCE_ATTRIBUTES: deployment.environment.name=local configs: - source: rum-api target: /etc/rum-api/config.yaml volumes: - rum-api-data:/home/nonroot depends_on: - loki - tempo - otel-collector
volumes: grafana-data: loki-data: tempo-data: mimir-data: rum-api-data:
configs: collector: # deploy/compose/collector/config.yaml at collector-v0.4.0 content: | # ouo-collector configuration for the local stack (ADR-0007: the collector is the edge). # # Image: ouo/collector, built from collector/ (collector/builder.yaml). Compose mounts this # file and projects.htpasswd into /etc/otelcol/. Ports: # 4318 OTLP/HTTP public Basic auth (project id : project token), CORS # 4319 replay public Basic auth, CORS, POST /v1/replay/chunks # 4320 Sentry public Sentry SDK envelopes (ADR-0008), Sentry DSN key auth, forwarded to Sentry # 4317 OTLP/gRPC loopback in compose, same Basic auth (send the Authorization metadata) # 13133 health loopback # 8888 self-metrics (Prometheus pull, not published; they are also pushed to Mimir) # # Environment: # OUO_INGEST_TOKEN bearer the ouoreplay exporter sends to rum-api (must equal rum-api's) # OUO_ALLOWED_ORIGINS optional comma-separated CORS origin list, no spaces # (default http://localhost:*,http://127.0.0.1:*) # OUO_RATELIMIT_CLIENT_KEY address (default) or x_forwarded_for behind a reverse proxy # OUO_RATELIMIT_{PROJECT,CLIENT}_{RATE,BURST} bucket sizes (defaults 1000/5000 and 100/300; # deploy/scripts/qa-edge.sh lowers them to test the buckets) # OUO_SENTRY_UPSTREAM Sentry ingest base URL the bridge forwards to (https://oNNN.ingest.sentry.io), # or "none" to answer locally (default none: no data leaves the laptop) # OUO_SENTRY_PROJECT_ID, OUO_SENTRY_PUBLIC_KEY the DSN's project id and public key # # Every receiver uses include_metadata: true so the resource processor and ouoratelimit # can read the authenticated project (auth.username) and request headers from the # client context. Anything that drops that context (batch) must come after them.
extensions: health_check: endpoint: 0.0.0.0:13133 basicauth/server: htpasswd: file: /etc/otelcol/projects.htpasswd # Receivers authenticate through ouoauth, which delegates to basicauth/server and # counts every rejected request (401) as ouo_auth_failures_total{reason} in Mimir. ouoauth: authenticator: basicauth/server # Receiver middleware sharing the ouoratelimit processor's buckets: the OTLP receiver # answers an empty export (no spans / records / points) 200 without calling the # pipeline, so only a middleware can charge it. Requests it charged are not charged # again by the processor; other receivers (replay, Sentry) are charged by the processor. ouoratelimit: processor: ouoratelimit
receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 include_metadata: true auth: authenticator: ouoauth middlewares: - id: ouoratelimit http: # 0.0.0.0 inside the container; exposure is decided by the compose port binding. endpoint: 0.0.0.0:4318 include_metadata: true max_request_body_size: 4194304 # 4 MiB, applied to the compressed and the decompressed body auth: authenticator: ouoauth middlewares: - id: ouoratelimit cors: allowed_origins: $${env:OUO_ALLOWED_ORIGINS:-http://localhost:*,http://127.0.0.1:*} allowed_headers: [Authorization, Content-Type, Content-Encoding, traceparent, baggage] exposed_headers: [Retry-After] max_age: 7200 ouoreplay: endpoint: 0.0.0.0:4319 include_metadata: true max_request_body_size: 8388608 # 8 MiB: frames chunks (rrweb chunks stop at max_rrweb_chunk_bytes, 4 MiB) auth: authenticator: ouoauth cors: allowed_origins: $${env:OUO_ALLOWED_ORIGINS:-http://localhost:*,http://127.0.0.1:*} allowed_headers: [Authorization, Content-Type, Content-Encoding, X-Ouo-Session-Id, X-Ouo-Seq] exposed_headers: [Retry-After] max_age: 7200 # Sentry SDK bridge (ADR-0008, docs/specs/sentry-bridge.md): apps change only their DSN host. # Every envelope is forwarded byte for byte to the project's upstream Sentry and Sentry's # answer goes back unchanged; accepted envelopes are translated into sessions, errors, # traces (logs/traces pipelines) and video replay chunks (routed to logs/replay). ouosentry: endpoint: 0.0.0.0:4320 upstream_timeout: 30s projects: - sentry_project_id: "$${env:OUO_SENTRY_PROJECT_ID:-1}" public_keys: ["$${env:OUO_SENTRY_PUBLIC_KEY:-publickey0000000000000000000000}"] upstream: $${env:OUO_SENTRY_UPSTREAM:-none} ouo_project_id: demo service_name: $${env:OUO_SENTRY_SERVICE_NAME:-sentry-demo}
connectors: # The Sentry bridge emits replay chunks on its logs output; send them to the replay # pipeline (ouoreplay exporter -> rum-api) and everything else to Loki. routing/sentry: default_pipelines: [logs] table: - context: log condition: log.attributes["ouo.replay.chunk"] == true pipelines: [logs/replay]
processors: memory_limiter: check_interval: 1s limit_mib: 512 spike_limit_mib: 128 # Token buckets per project (auth.username) and per client; over the limit the OTLP # receiver answers 429 + Retry-After (gRPC RESOURCE_EXHAUSTED + RetryInfo). All # pipelines share one set of buckets. Behind a reverse proxy (deploy/vm sets # OUO_RATELIMIT_CLIENT_KEY=x_forwarded_for) the TCP peer is the proxy, so key the # client bucket on the first X-Forwarded-For hop instead of the address. ouoratelimit: project: rate: $${env:OUO_RATELIMIT_PROJECT_RATE:-1000} burst: $${env:OUO_RATELIMIT_PROJECT_BURST:-5000} client: rate: $${env:OUO_RATELIMIT_CLIENT_RATE:-100} burst: $${env:OUO_RATELIMIT_CLIENT_BURST:-300} key_from: $${env:OUO_RATELIMIT_CLIENT_KEY:-address} idle_timeout: 10m # Stamp the authenticated project on every resource, overwriting whatever the client # sent (delete first so a request without auth context can never keep a spoofed value). resource/project: attributes: - key: ouo.project.id action: delete - key: ouo.project.id from_context: auth.username action: upsert batch: timeout: 1s send_batch_size: 512 send_batch_max_size: 1024 # Copy the OTLP top-level event_name (OTel Logs API `eventName`) into the # `event.name` log attribute so Loki can index it as a label regardless of # whether the SDK set it as a field or as an attribute. Also makes sure # `ouo.platform` exists as a log attribute when it was only set on the resource. transform/events: error_mode: ignore log_statements: - context: log statements: - set(log.attributes["event.name"], log.event_name) where log.event_name != "" and log.attributes["event.name"] == nil - set(log.attributes["ouo.platform"], resource.attributes["ouo.platform"]) where log.attributes["ouo.platform"] == nil and resource.attributes["ouo.platform"] != nil
# Collector >= 0.150 names the exporters otlp_grpc / otlp_http (otlp / otlphttp are deprecated aliases). exporters: otlp_grpc/tempo: endpoint: tempo:4317 tls: insecure: true otlp_http/loki: endpoint: http://loki:3100/otlp tls: insecure: true otlp_http/mimir: endpoint: http://mimir:9009/otlp tls: insecure: true # Replay chunks -> rum-api's internal sink (POST /v1/replay/chunks with # X-Ouo-Project-Id, X-Ouo-Session-Id, X-Ouo-Seq and the ingest bearer). ouoreplay: endpoint: http://rum-api:8080 ingest_token: $${env:OUO_INGEST_TOKEN} timeout: 30s sending_queue: enabled: true queue_size: 1000 retry_on_failure: enabled: true max_elapsed_time: 5m debug: verbosity: basic
service: extensions: [health_check, basicauth/server, ouoauth, ouoratelimit] telemetry: logs: level: info # Self-metrics (otelcol_receiver_*, otelcol_exporter_*, ouo_ratelimit_rejected_total, # ouo_auth_failures_total, ouo_replay_chunks_total): pushed to Mimir over OTLP every 15 s, and pullable on :8888 # inside the network for debugging. metrics: level: normal readers: - periodic: interval: 15000 exporter: otlp: protocol: http/protobuf endpoint: http://mimir:9009/otlp/v1/metrics - pull: exporter: prometheus: host: 0.0.0.0 port: 8888 pipelines: traces: receivers: [otlp, ouosentry] processors: [memory_limiter, ouoratelimit, resource/project, batch] exporters: [otlp_grpc/tempo] logs: receivers: [otlp, routing/sentry] processors: [memory_limiter, ouoratelimit, resource/project, transform/events, batch] exporters: [otlp_http/loki] metrics: receivers: [otlp] processors: [memory_limiter, ouoratelimit, resource/project, batch] exporters: [otlp_http/mimir] # Replay chunks: one log record per chunk, project stamped by the receiver. No batch: # the receiver waits for the queue hand-off and answers 202, 429 or 503 per chunk. logs/replay: receivers: [ouoreplay, routing/sentry] processors: [memory_limiter, ouoratelimit] exporters: [ouoreplay] # Sentry bridge output before routing; the project is in the client context # (auth.username), so resource/project and ouoratelimit work downstream. logs/sentry: receivers: [ouosentry] processors: [memory_limiter] exporters: [routing/sentry] projects-htpasswd: # deploy/compose/collector/projects.htpasswd at collector-v0.4.0 content: | # Development project registry for ouo-collector (basicauth extension, ADR-0007). # LOCAL USE ONLY. The demo token is public in this repository; never deploy this file. # One line per project. username = project id, password = project token, {SHA} hash. # Generate a line with htpasswd -nbs PROJECT TOKEN # Comment lines in this file must never contain a colon character (see project/kb/collector-components.md). demo:{SHA}IUpHBAUPHvXoJmvv4inmF4cN96I= # internal = the platform's own telemetry (rum-api, demo backend), token pt_dev_internal_00000000000000, # so it has its own rate-limit buckets and never spends a client project's quota. internal:{SHA}wXBw3q7GMLhIo7ifP3a0fC2Z14M= loki: # deploy/compose/loki/config.yaml at grafana-v0.5.0 content: | # Loki 3.x single-binary, filesystem storage, native OTLP ingestion. # Started with -config.expand-env=true: the OUO_* variables below come from compose.yaml (retention, # deletion timings; docs/operations/data-lifecycle.md). auth_enabled: false
server: http_listen_port: 3100 grpc_listen_port: 9096 log_level: warn
common: instance_addr: 127.0.0.1 path_prefix: /loki storage: filesystem: chunks_directory: /loki/chunks rules_directory: /loki/rules replication_factor: 1 ring: kvstore: store: inmemory
schema_config: configs: - from: "2024-01-01" store: tsdb object_store: filesystem schema: v13 index: prefix: index_ period: 24h
storage_config: tsdb_shipper: active_index_directory: /loki/tsdb-index cache_location: /loki/tsdb-cache
# Retention and deletion (roadmap 8b): the compactor applies limits_config.retention_period # (and per-project retention_stream entries from runtime.yaml) and processes delete # requests (POST /loki/api/v1/delete, sent by rum-api's delete-by-user / -project). compactor: working_directory: /loki/compactor compaction_interval: $${OUO_LOKI_COMPACTION_INTERVAL:-10m} retention_enabled: true # Chunks marked for deletion are removed after this delay. retention_delete_delay: $${OUO_LOKI_RETENTION_DELETE_DELAY:-2h} delete_request_store: filesystem # Grace period during which a delete request can be cancelled before the compactor # acts on it. delete_request_cancel_period: $${OUO_LOKI_DELETE_CANCEL_PERIOD:-24h}
# Per-project overrides (retention_stream on the ouo_project_id label), reloaded every # 10 s without a restart. runtime_config: file: /etc/loki/runtime.yaml period: 10s
pattern_ingester: enabled: true
ruler: storage: type: local local: directory: /loki/rules
limits_config: # Structured metadata is required for OTLP ingestion: every attribute that is # not promoted to a label lands there (session.id, user.id, trace_id, ...). allow_structured_metadata: true volume_enabled: true reject_old_samples: true reject_old_samples_max_age: 168h ingestion_rate_mb: 32 ingestion_burst_size_mb: 64 max_label_names_per_series: 30 # Retention of every stream (OUO_RETENTION_LOGS, at least 24h); runtime.yaml can set a # shorter or longer one per project. Delete requests remove lines from chunks. retention_period: $${OUO_RETENTION_LOGS:-720h} deletion_mode: filter-and-delete # Label promotion for OTLP (ADR-0001): only service_name, event_name, # ouo_platform and ouo_project_id (one value per project, for per-project # retention_stream and deletes) become stream labels; session.id stays # structured metadata so cardinality stays low. Loki rewrites "." to "_". otlp_config: resource_attributes: # Loki's default list also promotes service.instance.id, k8s.*, cloud.* which # would explode cardinality for browser SDKs; replace it with a fixed set. ignore_defaults: true attributes_config: - action: index_label attributes: - service.name - service.namespace - deployment.environment.name - ouo.platform - ouo.project.id log_attributes: - action: index_label attributes: - event.name - ouo.platform
analytics: reporting_enabled: false loki-runtime: # deploy/compose/loki/runtime.yaml at grafana-v0.5.0 content: | # Loki runtime overrides, reloaded every 10 s (runtime_config in config.yaml): edit and # save, no restart. Per-project log retention goes here as retention_stream entries on # the ouo_project_id stream label; the default is limits_config.retention_period # (OUO_RETENTION_LOGS). Loki's single tenant is "fake" (auth_enabled: false). # docs/operations/data-lifecycle.md, section Per-project retention. # # overrides: # fake: # retention_stream: # - selector: '{ouo_project_id="ttltest"}' # priority: 1 # period: 24h # at least 24h overrides: {} tempo: # deploy/compose/tempo/config.yaml at grafana-v0.5.0 content: | # Tempo 3.x single binary, local storage, OTLP receiver, metrics generator -> Mimir. # Started with -config.expand-env=true (trace retention from compose.yaml). # Only keys that exist in 3.0.x are used; see project/kb/collector-lgtm.md. # Grafana >= 11 streams search/trace results over gRPC on the HTTP port (h2c). stream_over_http_enabled: true
server: http_listen_port: 3200 log_level: warn
distributor: receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318
# Tempo 3.0 removed the 2.x `ingester` and `compactor` top-level blocks # (block-builder / live-store replace them); defaults are fine locally.
# Trace retention (roadmap 8b): blocks older than block_retention are deleted by the # backend scheduler's retention job (hourly) run on the backend worker. Both carry the # compaction settings in 3.0; keep them equal. Tempo has no delete API: single traces # are removed with `tempo-cli redact` (docs/operations/data-lifecycle.md). backend_scheduler: provider: compaction: compaction: block_retention: $${OUO_RETENTION_TRACES:-336h}
backend_worker: compaction: block_retention: $${OUO_RETENTION_TRACES:-336h}
querier: frontend_worker: frontend_address: 127.0.0.1:9095
query_frontend: search: duration_slo: 5s throughput_bytes_slo: 1.073741824e+09 trace_by_id: duration_slo: 5s
metrics_generator: registry: external_labels: source: tempo storage: path: /var/tempo/generator/wal remote_write: - url: http://mimir:9009/api/v1/push send_exemplars: true
storage: trace: backend: local wal: path: /var/tempo/wal local: path: /var/tempo/blocks
overrides: defaults: metrics_generator: processors: [service-graphs, span-metrics] mimir: # deploy/compose/mimir/config.yaml at grafana-v0.5.0 content: | # Mimir monolithic mode, filesystem storage, OTLP push at /otlp/v1/metrics. # Started with -config.expand-env=true (metric retention from compose.yaml). target: all multitenancy_enabled: false
server: http_listen_port: 9009 grpc_listen_port: 9095 log_level: warn
common: storage: backend: filesystem filesystem: dir: /data/blocks
blocks_storage: storage_prefix: blocks bucket_store: sync_dir: /data/tsdb-sync tsdb: dir: /data/tsdb
compactor: data_dir: /data/compactor sharding_ring: instance_addr: 127.0.0.1 kvstore: store: memberlist
distributor: ring: instance_addr: 127.0.0.1 kvstore: store: memberlist
ingester: ring: instance_addr: 127.0.0.1 replication_factor: 1 kvstore: store: memberlist
store_gateway: sharding_ring: instance_addr: 127.0.0.1 replication_factor: 1 kvstore: store: memberlist
ruler_storage: storage_prefix: rules
alertmanager_storage: storage_prefix: alertmanager
memberlist: bind_addr: [127.0.0.1] join_members: []
limits: # Accept metrics without OTel-style unit/type suffix mangling and keep resource attrs. otel_metric_suffixes_enabled: false max_global_series_per_user: 1000000 ingestion_rate: 100000 ingestion_burst_size: 500000 out_of_order_time_window: 5m # Metric retention (roadmap 8b): the compactor deletes blocks older than this. 0 keeps # everything. compactor_blocks_retention_period: $${OUO_RETENTION_METRICS:-2160h}
usage_stats: enabled: false rum-api: # deploy/compose/rum-api/config.yaml at rum-api-v0.5.0 content: | # rum-api configuration for the local stack, mounted at /etc/rum-api/config.yaml and # started with -config.file=/etc/rum-api/config.yaml -config.expand-env=true (like Loki # and Tempo). Every key and its environment override: services/rum-api/config.example.yaml. # Environment variables set in compose.yaml win over this file (GRAFANA_URL does, so the # deep links follow OUO_GRAFANA_PORT and deploy/vm/compose.tls.yaml's public domain).
server: http_listen_address: ":8080"
# Secrets come from the container environment (compose.yaml), never from this file. auth: ingest_token: "$${OUO_INGEST_TOKEN}" read_token: "$${OUO_READ_TOKEN}" admin_token: "$${OUO_ADMIN_TOKEN}"
loki: url: http://loki:3100 datasource_uid: loki
tempo: url: http://tempo:3200 datasource_uid: tempo
session: inactivity: 30m max_duration: 4h
storage: # filesystem: deploy/data/rum-api/replays on the host (bind mount, like Loki and Tempo # keep their local filesystem storage). compose.s3.yaml switches this to s3 # (STORAGE_BACKEND=s3) against the MinIO below. backend: filesystem filesystem: dir: /data s3: endpoint: minio:9000 bucket: $${OUO_RUM_API_S3_BUCKET:-ouo-replays} region: us-east-1 access_key_id: "$${S3_ACCESS_KEY_ID}" secret_access_key: "$${S3_SECRET_ACCESS_KEY}" insecure: true # plain HTTP inside the compose network force_path_style: true # MinIO
# Replay retention and the sweeper: REPLAY_RETENTION / LOG_RETENTION / REPLAY_SWEEP_INTERVAL # from compose.yaml (OUO_RETENTION_REPLAYS, OUO_RETENTION_LOGS) override these. retention: replays: 720h logs: 720h sweep_interval: 1h
# Per-project replay retention and server-side sampling caps (restart rum-api after an # edit). Keep a project's log retention in loki/runtime.yaml in step. projects: {} # ttltest: # retention: { replays: 24h } # sampling: { session_rate: 1, replay_rate: 0.25 } grafana-datasources: # deploy/compose/grafana/provisioning/datasources/datasources.yaml at grafana-v0.5.0 content: | apiVersion: 1
deleteDatasources: - name: Loki orgId: 1 - name: Tempo orgId: 1 - name: Mimir orgId: 1
datasources: - name: Loki uid: loki type: loki access: proxy url: http://loki:3100 editable: false jsonData: maxLines: 1000 derivedFields: # OTLP log records carry the trace id as structured metadata `trace_id`. - name: TraceID datasourceUid: tempo matcherType: label matcherRegex: trace_id url: "$$$${__value.raw}" urlDisplayLabel: View trace
- name: Tempo uid: tempo type: tempo access: proxy url: http://tempo:3200 editable: false jsonData: streamingEnabled: search: true nodeGraph: enabled: true serviceMap: datasourceUid: mimir tracesToMetrics: datasourceUid: mimir spanStartTimeShift: "-5m" spanEndTimeShift: "5m" lokiSearch: datasourceUid: loki tracesToLogsV2: datasourceUid: loki spanStartTimeShift: "-30m" spanEndTimeShift: "30m" filterByTraceID: false filterBySpanID: false customQuery: true # `$${__span.tags[...]}` resolves both span attributes (session.id) and resource # attributes (service.name): Grafana merges process tags into __span.tags and the # field path goes through lodash `property`, so bracket keys with dots work. # session.id is structured metadata in Loki, so it is a `|` filter, never a # stream selector. `$$$$` escapes the provisioner's own $${ENV} expansion. tags: - key: service.name value: service_name query: '{service_name="$$$${__span.tags["service.name"]}"} | session_id="$$$${__span.tags["session.id"]}"'
- name: Mimir uid: mimir type: prometheus access: proxy url: http://mimir:9009/prometheus editable: false jsonData: httpMethod: POST prometheusType: Mimir prometheusVersion: "2.9.1" timeInterval: 15s exemplarTraceIdDestination: - name: trace_id datasourceUid: tempo grafana-plugin-app: # deploy/compose/grafana/provisioning/plugins/ouo11y-rum-app.yaml at grafana-v0.5.0 content: | # Enables the ouo11y RUM app plugin (plugins/ouo11y-rum-app, loaded unsigned from # /var/lib/grafana/plugins/ouo11y-rum-app) and points its "rum" route at the # in-network rum-api. The browser never talks to rum-api directly: the plugin # calls /api/plugin-proxy/ouo11y-rum-app/rum/v1/... and Grafana forwards to rumApiUrl, # adding `Authorization: Bearer <readToken>` from secureJsonData (ADR-0007 section 6). # # $${OUO_READ_TOKEN} is expanded by the Grafana provisioner from the container's # environment (compose passes it through; default rt_dev_0000000000000000, a # development value). It must equal rum-api's OUO_READ_TOKEN. Secure fields are # write-only: the settings API reports `secureJsonFields: {readToken: true}`. # # $${OUO_FEATURE_TOGGLES_ENABLE} turns plugin feature toggles on, comma separated, like # Grafana's GF_FEATURE_TOGGLES_ENABLE (empty: all off). The list is in # plugins/ouo11y-rum-app/README.md, e.g. OUO_FEATURE_TOGGLES_ENABLE=sessionTimelinePanel. apiVersion: 1
apps: - type: ouo11y-rum-app org_id: 1 disabled: false jsonData: rumApiUrl: http://rum-api:8080 featureToggles: "$${OUO_FEATURE_TOGGLES_ENABLE}" secureJsonData: readToken: $${OUO_READ_TOKEN}The comments at its top say which ouo11y release each image and config comes from, and how to pin them.
4b. Log in to ghcr.io
Section titled “4b. Log in to ghcr.io”With access, create a classic GitHub token with only the read:packages scope. Put the token in your shell (export GHCR_TOKEN=...), never in a file, then:
echo $GHCR_TOKEN | docker login ghcr.io -u <your-github-user> --password-stdinReplace <your-github-user> with your GitHub user name. It prints Login Succeeded.
4c. Check the ports
Section titled “4c. Check the ports”The stack publishes 3000 (Grafana), 3100, 3200, 9009, 8080, and the collector’s 4318 (OTLP), 4319 (replay) and 4320. This prints nothing when they are all free:
lsof -nP -iTCP -sTCP:LISTEN | grep -E ':(3000|3100|3200|4318|4319|4320|8080|9009) 'If something is listed (often another collector on 4318-4320), do not stop it: move this stack instead. Create stack/.env:
code ~/Desktop/tiny-shop/stack/.envwith:
OUO_OTLP_HTTP_PORT=14318OUO_REPLAY_PORT=14319OUO_SENTRY_PORT=14320OUO_OTLP_GRPC_PORT=14317OUO_COLLECTOR_HEALTH_PORT=14133Compose reads .env on its own. Remember the new collector ports: pages 3 and 4 tell you where they go. Another port listed (3000, 8080, …)? Its variable is OUO_GRAFANA_PORT, OUO_RUM_API_PORT, … (all of them: the ${OUO_..._PORT:-...} lines in compose.yaml).
4d. Start it
Section titled “4d. Start it”In the third terminal tab:
cd ~/Desktop/tiny-shop/stackdocker compose up -dThe first start pulls the images, which takes a few minutes. Then:
docker compose ps -aWhat you should see
Section titled “What you should see”grafana and rum-api become healthy; loki, tempo, mimir and otel-collector are Up; grafana-plugin is Exited (0), which is right: it only copies the ouo11y plugin into Grafana and stops. If grafana still says starting, run docker compose ps -a again after a few seconds.
Open http://localhost:3000 and log in with admin / testing567. More apps → ouo11y RUM is there and still empty: nothing sends it data yet. The next page changes that, starting with the API.