diff --git a/.github/renovate-tracked-deps.json b/.github/renovate-tracked-deps.json index ba200586..c1fe0edc 100644 --- a/.github/renovate-tracked-deps.json +++ b/.github/renovate-tracked-deps.json @@ -276,6 +276,7 @@ }, "mise.toml": { "mise": [ + "bats", "go", "go:github.com/grafana/oats", "java", diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 04f22bf3..9e6e036f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,8 +26,8 @@ jobs: version: v2026.3.16 sha256: 1aadc8f126b0fc588b70ad2296cf7a963ba014ef2fd017a6087329ec6160063e - - name: Lint + - name: CI checks env: GITHUB_TOKEN: ${{ github.token }} GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: mise run lint + run: mise run ci diff --git a/AGENTS.md b/AGENTS.md index 8dadd194..cb516605 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,11 +49,17 @@ mise run fix # Verify only (same command used in CI) mise run lint + +# Run without Docker/Podman (e.g. inside a container) +NATIVE=true mise run lint:fast ``` After running `fix`, always review the changed files before committing — auto-fixes may produce unexpected results. +Native mode requires lint tools on PATH. Run `mise run setup:native-lint-tools` +once to install them. + Go code uses `.golangci.yaml` config. Markdown uses `.markdownlint.yaml`. EditorConfig rules in `.editorconfig`. @@ -77,6 +83,7 @@ for other components. Each component has a `run-*.sh` startup script. ### Example Applications (examples/) Language-specific demo apps that emit OpenTelemetry data: + - `examples/java` (port 8080) - Maven + OTel Java Agent - `examples/go` (port 8081) - Go workspace (`go.work` at repository root) - `examples/python` (port 8082) - Python + auto-instrumentation diff --git a/README.md b/README.md index d34c306c..4b67147c 100644 --- a/README.md +++ b/README.md @@ -139,9 +139,9 @@ to the specified endpoint using "OTLP/HTTP". You can also configure per-signal endpoints: -* `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` -* `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` -* `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` +- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` +- `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` +- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` If both global and per-signal endpoints are set, per-signal values take precedence. Endpoints must include the scheme (for example, `http://jaeger:4318`). @@ -257,7 +257,7 @@ services: kubectl apply -f k8s/lgtm.yaml # Configure port forwarding -kubectl port-forward service/lgtm 3000:3000 4040:4040 4317:4317 4318:4318 9090:9090 +kubectl port-forward service/lgtm 3000:3000 3200:3200 4040:4040 4317:4317 4318:4318 9090:9090 # Using mise mise k8s-apply @@ -387,6 +387,19 @@ OIDC_ISSUER="https://token.actions.githubusercontent.com" cosign verify ${IMAGE} --certificate-identity ${IDENTITY} --certificate-oidc-issuer ${OIDC_ISSUER} ``` +## AI Tool Integration (MCP) + +The stack provides an [MCP][mcp] integration so AI coding tools can query logs, metrics, traces, +and dashboards. Tempo exposes an HTTP MCP endpoint from the container, while Grafana +dashboards and queries are accessed via a client-side MCP server (`uvx mcp-grafana`). + +```sh +docker exec lgtm cat /etc/lgtm/mcp.json # or: podman exec ... +# Kubernetes: kubectl exec deploy/lgtm -- cat /etc/lgtm/mcp.json +``` + +Paste the JSON into your AI tool's MCP configuration. See [docs/mcp-integration.md](docs/mcp-integration.md) for details. + ## Related Work - [Metrics, Logs, Traces and Profiles in Grafana][mltp] @@ -406,6 +419,7 @@ cosign verify ${IMAGE} --certificate-identity ${IDENTITY} --certificate-oidc-iss [grafana-env-overrides]: https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#override-configuration-with-environment-variables [grafana-preinstall-plugins]: https://grafana.com/docs/grafana/latest/setup-grafana/configure-docker/#install-plugins-in-the-docker-container [java-example]: examples/java/ +[mcp]: https://modelcontextprotocol.io/ "Model Context Protocol" [mise]: https://github.com/jdx/mise [mltp]: https://github.com/grafana/intro-to-mltp [otel-checker]: https://github.com/grafana/otel-checker/ diff --git a/docker/run-all.sh b/docker/run-all.sh index bf9b5d72..86e6de60 100755 --- a/docker/run-all.sh +++ b/docker/run-all.sh @@ -142,6 +142,78 @@ echo "Total: ${total_elapsed} seconds" touch /tmp/ready echo "The OpenTelemetry collector and the Grafana LGTM stack are up and running. (created /tmp/ready)" +# Create a service account token and MCP config for AI tool access +GRAFANA_CREDS="${GF_SECURITY_ADMIN_USER:-admin}:${GF_SECURITY_ADMIN_PASSWORD:-admin}" +GRAFANA_URL="${GRAFANA_URL:-http://127.0.0.1:3000}" +GRAFANA_PUBLIC_URL="${GRAFANA_PUBLIC_URL:-http://localhost:3000}" +TEMPO_URL="${TEMPO_URL:-http://localhost:3200}" +SA_NAME="ai-tools" +SA_TOKEN_NAME="ai-tools-token" +GRAFANA_SA_URL="${GRAFANA_URL}/api/serviceaccounts" +# Try to create SA; if it already exists (persisted data), look it up +SA_RESPONSE="$(curl -sf "${GRAFANA_SA_URL}" \ + -H "Content-Type: application/json" -u "${GRAFANA_CREDS}" \ + -d "{\"name\":\"${SA_NAME}\",\"role\":\"Viewer\"}")" +if [ -z "$SA_RESPONSE" ]; then + # SA already exists — find its ID + SA_RESPONSE="$(curl -sf "${GRAFANA_SA_URL}/search?query=${SA_NAME}" -u "${GRAFANA_CREDS}")" +fi +SA_ID="$(echo "$SA_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)" +if [ -n "$SA_ID" ]; then + # Delete only the bootstrap-managed token (preserve any manually-created tokens) + EXISTING_TOKENS="$(curl -sf "${GRAFANA_SA_URL}/${SA_ID}/tokens" -u "${GRAFANA_CREDS}")" + if [ -n "$EXISTING_TOKENS" ]; then + BOOTSTRAP_TOKEN_ID="$(echo "$EXISTING_TOKENS" | tr '{}' '\n' | + grep "\"name\":\"${SA_TOKEN_NAME}\"" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)" + if [ -n "$BOOTSTRAP_TOKEN_ID" ]; then + curl -sf -X DELETE "${GRAFANA_SA_URL}/${SA_ID}/tokens/${BOOTSTRAP_TOKEN_ID}" \ + -u "${GRAFANA_CREDS}" >/dev/null + fi + fi + TOKEN_RESPONSE="$(curl -sf "${GRAFANA_SA_URL}/${SA_ID}/tokens" \ + -H "Content-Type: application/json" -u "${GRAFANA_CREDS}" \ + -d "{\"name\":\"${SA_TOKEN_NAME}\"}")" + SA_TOKEN="$(echo "$TOKEN_RESPONSE" | grep -o '"key":"[^"]*"' | cut -d'"' -f4)" + if [ -n "$SA_TOKEN" ]; then + EXEC="${CONTAINER_RUNTIME:-docker} exec lgtm" + ( + umask 077 + mkdir -p /etc/lgtm + echo "${SA_TOKEN}" >/tmp/grafana-sa-token + cat >/etc/lgtm/mcp.json <<-MCPEOF + { + "mcpServers": { + "grafana": { + "command": "uvx", + "args": ["mcp-grafana"], + "env": { + "GRAFANA_URL": "${GRAFANA_PUBLIC_URL}", + "GRAFANA_SERVICE_ACCOUNT_TOKEN": "${SA_TOKEN}" + } + }, + "tempo": { + "url": "${TEMPO_URL}/api/mcp" + } + } + } + MCPEOF + cat >/etc/lgtm/claude-mcp-setup.sh <<-SETUPEOF + #!/bin/bash + # Connect Claude Code to the LGTM stack + claude mcp add grafana -e "GRAFANA_URL=${GRAFANA_PUBLIC_URL}" -e "GRAFANA_SERVICE_ACCOUNT_TOKEN=${SA_TOKEN}" -- uvx mcp-grafana + claude mcp add --transport http tempo "${TEMPO_URL}/api/mcp" + SETUPEOF + ) + echo "" + echo "AI Tool Integration (MCP):" + echo " Claude Code: bash <($EXEC cat /etc/lgtm/claude-mcp-setup.sh)" + echo " Other tools: $EXEC cat /etc/lgtm/mcp.json" + docs_ref="main" + [[ -n "${LGTM_VERSION}" ]] && docs_ref="v${LGTM_VERSION}" + echo " Docs: https://github.com/grafana/docker-otel-lgtm/blob/${docs_ref}/docs/mcp-integration.md" + fi +fi + if [[ ${ENABLE_OBI:-false} == "true" ]]; then # Non-blocking check — don't delay readiness if OBI fails (e.g. missing capabilities) if curl -o /dev/null -sg "http://127.0.0.1:6060/metrics" -w "%{response_code}" 2>/dev/null | grep -q "200"; then @@ -164,6 +236,7 @@ echo "Open ports:" echo " - 4317: OpenTelemetry GRPC endpoint" echo " - 4318: OpenTelemetry HTTP endpoint" echo " - 3000: Grafana (http://localhost:3000). User: admin, password: admin" +echo " - 3200: Tempo endpoint (MCP at http://localhost:3200/api/mcp)" echo " - 4040: Pyroscope endpoint" echo " - 9090: Prometheus endpoint" diff --git a/docker/run-otelcol.bats b/docker/run-otelcol.bats new file mode 100644 index 00000000..7e92e1ff --- /dev/null +++ b/docker/run-otelcol.bats @@ -0,0 +1,146 @@ +#!/usr/bin/env bats +# shellcheck disable=SC2030,SC2031 # export in @bats test is intentionally local +bats_require_minimum_version 1.5.0 +# Tests for run-otelcol.sh config generation logic. +# The script is run in a temp dir with a stub logging.sh so that +# run_with_logging records its args without exec-ing the real binary. + +setup() { + TESTDIR=$(mktemp -d) + + # Stub logging.sh: record otelcol args to a file instead of exec-ing. + cat >"$TESTDIR/logging.sh" <"$TESTDIR/otelcol-invocation" +} +EOF + + cp "$BATS_TEST_DIRNAME/run-otelcol.sh" "$TESTDIR/" + + export OPENTELEMETRY_COLLECTOR_VERSION="test" + unset OTEL_EXPORTER_OTLP_ENDPOINT \ + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT \ + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT \ + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT \ + OTEL_EXPORTER_OTLP_HEADERS \ + OTEL_COLLECTOR_DEBUG_EXPORTER \ + OTELCOL_EXTRA_ARGS 2>/dev/null || true +} + +teardown() { + rm -rf "$TESTDIR" +} + +run_otelcol() { + cd "$TESTDIR" && bash run-otelcol.sh +} + +# --- no flags --- + +@test "no flags: no overlay config generated" { + run run_otelcol + [ "$status" -eq 0 ] + [ ! -f "$TESTDIR/otelcol-config-export-http.yaml" ] +} + +@test "no flags: otelcol started with only base config" { + run run_otelcol + [ "$status" -eq 0 ] + grep -q -- "--config=file:./otelcol-config.yaml" "$TESTDIR/otelcol-invocation" + run ! grep -q "otelcol-config-export-http" "$TESTDIR/otelcol-invocation" +} + +# --- debug only --- + +@test "debug only: overlay generated" { + export OTEL_COLLECTOR_DEBUG_EXPORTER=true + run run_otelcol + [ "$status" -eq 0 ] + [ -f "$TESTDIR/otelcol-config-export-http.yaml" ] +} + +@test "debug only: overlay has debug exporters for all signals" { + export OTEL_COLLECTOR_DEBUG_EXPORTER=true + run run_otelcol + grep -q "debug/traces" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "debug/metrics" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "debug/logs" "$TESTDIR/otelcol-config-export-http.yaml" +} + +@test "debug only: overlay has no external exporters" { + export OTEL_COLLECTOR_DEBUG_EXPORTER=true + run run_otelcol + run ! grep -q "otlphttp/external" "$TESTDIR/otelcol-config-export-http.yaml" +} + +@test "debug only: overlay passed to otelcol" { + export OTEL_COLLECTOR_DEBUG_EXPORTER=true + run run_otelcol + grep -q "otelcol-config-export-http" "$TESTDIR/otelcol-invocation" +} + +# --- external only (shared endpoint) --- + +@test "external only: overlay has external exporters for all signals" { + export OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 + run run_otelcol + grep -q "otlphttp/external-traces" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "otlphttp/external-metrics" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "otlphttp/external-logs" "$TESTDIR/otelcol-config-export-http.yaml" +} + +@test "external only: overlay has no debug exporters" { + export OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 + run run_otelcol + run ! grep -q "debug/" "$TESTDIR/otelcol-config-export-http.yaml" +} + +# --- per-signal external endpoints --- + +@test "per-signal: only configured signals get external exporter" { + export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://tempo:4318 + run run_otelcol + grep -q "otlphttp/external-traces" "$TESTDIR/otelcol-config-export-http.yaml" + run ! grep -q "otlphttp/external-metrics" "$TESTDIR/otelcol-config-export-http.yaml" + run ! grep -q "otlphttp/external-logs" "$TESTDIR/otelcol-config-export-http.yaml" +} + +# --- both debug and external --- + +@test "both: overlay has debug and external exporters for all signals" { + export OTEL_COLLECTOR_DEBUG_EXPORTER=true + export OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 + run run_otelcol + grep -q "debug/traces" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "debug/metrics" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "debug/logs" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "otlphttp/external-traces" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "otlphttp/external-metrics" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "otlphttp/external-logs" "$TESTDIR/otelcol-config-export-http.yaml" +} + +@test "both: only one overlay config passed to otelcol" { + export OTEL_COLLECTOR_DEBUG_EXPORTER=true + export OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 + run run_otelcol + count=$(grep -c "otelcol-config-export-http" "$TESTDIR/otelcol-invocation") + [ "$count" -eq 1 ] +} + +# --- headers --- + +@test "headers: added to external endpoints in overlay" { + export OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 + export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer token123" + run run_otelcol + grep -q "headers:" "$TESTDIR/otelcol-config-export-http.yaml" + grep -q "Authorization" "$TESTDIR/otelcol-config-export-http.yaml" +} + +@test "headers: not applied when debug only (no external endpoints)" { + export OTEL_COLLECTOR_DEBUG_EXPORTER=true + export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer token123" + run run_otelcol + run ! grep -q "headers:" "$TESTDIR/otelcol-config-export-http.yaml" +} diff --git a/docker/run-otelcol.sh b/docker/run-otelcol.sh index 345228c4..181b1a43 100755 --- a/docker/run-otelcol.sh +++ b/docker/run-otelcol.sh @@ -1,35 +1,36 @@ #!/bin/bash +# shellcheck disable=SC1091 source ./logging.sh secondary_config_file="" -render_external_otlp_export_config() { - cat <<'EOF' >otelcol-config-export-http.yaml -service: - pipelines: -EOF +render_pipeline_overlay() { + printf 'service:\n pipelines:\n' >otelcol-config-export-http.yaml for signal in traces metrics logs; do local signal_var="OTEL_EXPORTER_OTLP_${signal^^}_ENDPOINT" - if [[ -n ${!signal_var:-} ]]; then - printf ' %s:\n exporters: [otlphttp/%s, otlphttp/external-%s]\n' "${signal}" "${signal}" "${signal}" >>otelcol-config-export-http.yaml + if [[ -n ${!signal_var:-} || ${OTEL_COLLECTOR_DEBUG_EXPORTER:-false} == "true" ]]; then + local exporters="otlphttp/${signal}" + [[ -n ${!signal_var:-} ]] && exporters+=", otlphttp/external-${signal}" + [[ ${OTEL_COLLECTOR_DEBUG_EXPORTER:-false} == "true" ]] && exporters+=", debug/${signal}" + printf ' %s:\n exporters: [%s]\n' "${signal}" "${exporters}" >>otelcol-config-export-http.yaml fi done - cat <<'EOF' >>otelcol-config-export-http.yaml - -exporters: -EOF - - for signal in traces metrics logs; do - local signal_var="OTEL_EXPORTER_OTLP_${signal^^}_ENDPOINT" - if [[ -n ${!signal_var:-} ]]; then - # shellcheck disable=SC2016 # otelcol config template, not bash variables - printf ' otlphttp/external-%s:\n endpoint: ${env:%s}\n' \ - "${signal}" "${signal_var}" >>otelcol-config-export-http.yaml - fi - done + if [[ -n ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-} || + -n ${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-} || + -n ${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-} ]]; then + printf '\nexporters:\n' >>otelcol-config-export-http.yaml + for signal in traces metrics logs; do + local signal_var="OTEL_EXPORTER_OTLP_${signal^^}_ENDPOINT" + if [[ -n ${!signal_var:-} ]]; then + # shellcheck disable=SC2016 # otelcol config template, not bash variables + printf ' otlphttp/external-%s:\n endpoint: ${env:%s}\n' \ + "${signal}" "${signal_var}" >>otelcol-config-export-http.yaml + fi + done + fi } if [[ -n ${OTEL_EXPORTER_OTLP_ENDPOINT:-} || @@ -45,11 +46,23 @@ if [[ -n ${OTEL_EXPORTER_OTLP_ENDPOINT:-} || export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-${OTEL_EXPORTER_OTLP_ENDPOINT:-}}" export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-${OTEL_EXPORTER_OTLP_ENDPOINT:-}}" export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-${OTEL_EXPORTER_OTLP_ENDPOINT:-}}" +fi - render_external_otlp_export_config +if [[ ${OTEL_COLLECTOR_DEBUG_EXPORTER:-false} == "true" ]]; then + echo "Enabling debug exporter for OpenTelemetry Collector" +fi + +if [[ -n ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-} || + -n ${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-} || + -n ${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-} || + ${OTEL_COLLECTOR_DEBUG_EXPORTER:-false} == "true" ]]; then + render_pipeline_overlay secondary_config_file="--config=file:./otelcol-config-export-http.yaml" - if [[ -v OTEL_EXPORTER_OTLP_HEADERS && -n ${OTEL_EXPORTER_OTLP_HEADERS} ]]; then + if [[ -v OTEL_EXPORTER_OTLP_HEADERS && -n ${OTEL_EXPORTER_OTLP_HEADERS} && + (-n ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-} || + -n ${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-} || + -n ${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-}) ]]; then echo "Adding headers from OTEL_EXPORTER_OTLP_HEADERS" yaml_headers="{" diff --git a/docker/tempo-config.yaml b/docker/tempo-config.yaml index 27fdae07..c3cba065 100644 --- a/docker/tempo-config.yaml +++ b/docker/tempo-config.yaml @@ -27,6 +27,10 @@ memberlist: bind_addr: [127.0.0.1] bind_port: 7947 +query_frontend: + mcp_server: + enabled: true + querier: frontend_worker: frontend_address: 127.0.0.1:9096 diff --git a/docs/mcp-integration.md b/docs/mcp-integration.md new file mode 100644 index 00000000..99742e9e --- /dev/null +++ b/docs/mcp-integration.md @@ -0,0 +1,92 @@ +# AI Tool Integration (MCP) + +The `grafana/otel-lgtm` image integrates with [Model Context Protocol (MCP)][mcp] +so AI coding tools (Claude, Cursor, etc.) can query your telemetry data directly. + +Tempo exposes an HTTP MCP endpoint from inside the container. Grafana data is accessed +via a client-side `uvx mcp-grafana` process that runs on your machine and connects to +the Grafana instance in the container. + +## What you get + +- **Dashboards**: list, read, and search dashboards via client-side `uvx mcp-grafana` +- **Logs**: query via LogQL via client-side `uvx mcp-grafana` +- **Metrics**: query via PromQL via client-side `uvx mcp-grafana` +- **Traces**: query via TraceQL through Tempo's built-in HTTP MCP endpoint (in-container) + +## Setup + +1. Start the container: + + ```sh + ./run-lgtm.sh + ``` + +2. Get the MCP configuration: + + ```sh + docker exec lgtm cat /etc/lgtm/mcp.json # or: podman exec ... + ``` + +3. Paste the JSON into your AI tool's MCP configuration. + + For Claude Code, you can add the servers individually: + + ```sh + # Get the service account token + TOKEN=$(docker exec lgtm cat /tmp/grafana-sa-token) # or: podman exec ... + + # Add the Grafana MCP server (requires uvx) + claude mcp add grafana \ + -e GRAFANA_URL=http://localhost:3000 \ + -e GRAFANA_SERVICE_ACCOUNT_TOKEN="$TOKEN" \ + -- uvx mcp-grafana + + # Add the Tempo MCP server + claude mcp add --transport http tempo http://localhost:3200/api/mcp + ``` + +## Backend mapping + +| Component | MCP Server | Transport | What you can query | +|------------|---------------|-----------|-------------------------------------| +| Grafana | `grafana` | stdio | Dashboards, PromQL, LogQL | +| Tempo | `tempo` | HTTP | Traces via TraceQL | + +## Collector debug exporter + +The OpenTelemetry Collector includes a debug exporter that logs all received +telemetry to stdout. This is useful for verifying that data is flowing correctly. + +Enable it by setting the environment variable before starting the container: + +```sh +OTEL_COLLECTOR_DEBUG_EXPORTER=true ./run-lgtm.sh +``` + +This adds the `debug` exporter to the logs, metrics, and traces pipelines. +The output appears in the collector's logs (enable with `ENABLE_LOGS_OTELCOL=true` +or `ENABLE_LOGS_ALL=true`). + +## OBI (eBPF auto-instrumentation) + +When [OBI is enabled][obi-readme], it generates traces and RED metrics automatically. +These are queryable via PromQL through the Grafana MCP server: + +```promql +# Number of instrumented processes +obi_instrumented_processes + +# HTTP request duration (RED metrics) +http_server_request_duration_seconds_count{http_route="/rolldice"} +``` + +See the [OBI section in the README][obi-readme] for setup instructions. + +## Pyroscope (continuous profiling) + +Pyroscope collects continuous profiles on port 4040. Explore them in Grafana's +**Explore > Profiles** view. There is no MCP integration for Pyroscope yet. + +[mcp]: https://modelcontextprotocol.io/ +[obi-readme]: ../README.md#enable-obi-ebpf-auto-instrumentation diff --git a/k8s/lgtm.yaml b/k8s/lgtm.yaml index 9be0a5c7..7b7d09b7 100644 --- a/k8s/lgtm.yaml +++ b/k8s/lgtm.yaml @@ -11,6 +11,10 @@ spec: protocol: TCP port: 3000 targetPort: 3000 + - name: tempo + protocol: TCP + port: 3200 + targetPort: 3200 - name: pyroscope protocol: TCP port: 4040 @@ -47,6 +51,7 @@ spec: image: grafana/otel-lgtm:latest ports: - containerPort: 3000 + - containerPort: 3200 - containerPort: 4040 - containerPort: 4317 - containerPort: 4318 diff --git a/mise.toml b/mise.toml index abfb91d4..f0ea6396 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,6 @@ # pin all versions to avoid GitHub rate limit [tools] +bats = "1.13.0" go = "1.26.1" "go:github.com/grafana/oats" = "0.6.1" java = "temurin-25.0.2+10.0.LTS" @@ -16,17 +17,17 @@ SUPER_LINTER_VERSION="v8.4.0@sha256:c5e3307932203ff9e1e8acfe7e92e894add6266605b5 # Shared lint tasks from flint (https://github.com/grafana/flint) [tasks."lint:super-linter"] description = "Run Super-Linter on the repository" -file = "https://raw.githubusercontent.com/grafana/flint/8a39d96e17327c54974623b252eb5260d67ed68a/tasks/lint/super-linter.sh" # v0.9.1 +file = "https://raw.githubusercontent.com/grafana/flint/df7b637ee421a491ca5c34b5be4122d8360f57c1/tasks/lint/super-linter.sh" # v0.9.2 [tasks."lint:links"] description = "Check for broken links in changed files + all local links" -file = "https://raw.githubusercontent.com/grafana/flint/8a39d96e17327c54974623b252eb5260d67ed68a/tasks/lint/links.sh" # v0.9.1 +file = "https://raw.githubusercontent.com/grafana/flint/df7b637ee421a491ca5c34b5be4122d8360f57c1/tasks/lint/links.sh" # v0.9.2 [tasks."lint:renovate-deps"] description = "Verify renovate-tracked-deps.json is up to date" -file = "https://raw.githubusercontent.com/grafana/flint/8a39d96e17327c54974623b252eb5260d67ed68a/tasks/lint/renovate-deps.py" # v0.9.1 +file = "https://raw.githubusercontent.com/grafana/flint/df7b637ee421a491ca5c34b5be4122d8360f57c1/tasks/lint/renovate-deps.py" # v0.9.2 [tasks."setup:native-lint-tools"] description = "Install native lint tools matching the pinned super-linter version" -file = "https://raw.githubusercontent.com/grafana/flint/8a39d96e17327c54974623b252eb5260d67ed68a/tasks/setup/native-lint-tools.sh" # v0.9.1 +file = "https://raw.githubusercontent.com/grafana/flint/df7b637ee421a491ca5c34b5be4122d8360f57c1/tasks/setup/native-lint-tools.sh" # v0.9.2 [tasks."lint:fast"] description = "Run fast lints (no Renovate)" depends = ["lint:super-linter", "lint:links"] @@ -43,6 +44,14 @@ run = "NATIVE=true mise run lint:fast" description = "Install git pre-commit hook that runs native linting" run = "mise generate git-pre-commit --write --task=pre-commit" +[tasks."test:unit"] +description = "Run unit tests" +run = "bats docker/run-otelcol.bats" + +[tasks."ci"] +description = "Run all checks (lint + unit tests)" +depends = ["lint", "test:unit"] + [tasks."lint"] description = "Run all lints" depends = ["lint:fast", "lint:renovate-deps"] @@ -108,7 +117,7 @@ run = "kubectl apply -f k8s/lgtm.yaml" [tasks.k8s-port-forward] description = "Port-forward LGTM Kubernetes service" -run = "kubectl port-forward service/lgtm 3000:3000 4040:4040 4317:4317 4318:4318 9090:9090" +run = "kubectl port-forward service/lgtm 3000:3000 3200:3200 4040:4040 4317:4317 4318:4318 9090:9090" [tasks.build-lgtm] description = "Build LGTM image" diff --git a/run-lgtm.ps1 b/run-lgtm.ps1 index c2864259..8e664261 100644 --- a/run-lgtm.ps1 +++ b/run-lgtm.ps1 @@ -68,6 +68,7 @@ if ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected) { $runCommand = @( 'container', 'run' '--name', 'lgtm' + '--init' ) # Append OBI-related flags (if any) so each flag is a separate argument @@ -78,10 +79,12 @@ if ($obiFlags.Count -gt 0) { # Append the remaining fixed arguments $runCommand += @( '-p', '3000:3000' + '-p', '3200:3200' '-p', '4040:4040' '-p', '4317:4317' '-p', '4318:4318' '-p', '9090:9090' + '-e', "CONTAINER_RUNTIME=$(Split-Path -Leaf $containerCommand)" '--rm' ) diff --git a/run-lgtm.sh b/run-lgtm.sh index d8ab7001..112bef85 100755 --- a/run-lgtm.sh +++ b/run-lgtm.sh @@ -61,9 +61,11 @@ fi $RUNTIME container run \ --name lgtm \ + --init \ "${OBI_FLAGS[@]}" \ "${OBI_ENV_FLAGS[@]}" \ -p 3000:3000 \ + -p 3200:3200 \ -p 4040:4040 \ -p 4317:4317 \ -p 4318:4318 \ @@ -74,5 +76,7 @@ $RUNTIME container run \ -v "${LOCAL_VOLUME}"/prometheus:/data/prometheus:"${MOUNT_OPTS}" \ -v "${LOCAL_VOLUME}"/loki:/data/loki:"${MOUNT_OPTS}" \ -e GF_PATHS_DATA=/data/grafana \ + -e CONTAINER_RUNTIME="$RUNTIME" \ + -e OTEL_COLLECTOR_DEBUG_EXPORTER="${OTEL_COLLECTOR_DEBUG_EXPORTER:-}" \ --env-file .env \ "$IMAGE"