Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
44540db
feat: MCP integration for AI tools (#1091)
zeitlinger Mar 12, 2026
d5f8184
style: fix shfmt and line-length lint failures
zeitlinger Mar 16, 2026
382c35c
merge: resolve conflict in run-otelcol.sh
zeitlinger Mar 16, 2026
225a452
address review comments on MCP integration
zeitlinger Mar 18, 2026
69a55ba
fix: only delete bootstrap-managed SA token on restart
zeitlinger Mar 20, 2026
a63c766
style: fix markdown list style and shellcheck warning
zeitlinger Mar 20, 2026
aeff707
address review comments: env var overrides, style fixes, doc improvem…
zeitlinger Mar 31, 2026
3b64d3c
fix: re-enable SC1091 after source
zeitlinger Mar 31, 2026
c19471a
fix: use GF_SECURITY_ADMIN_USER/PASSWORD for SA token creation
zeitlinger Mar 31, 2026
bc65cc8
fix: inline shellcheck disable for source
zeitlinger Mar 31, 2026
80d5bd2
fix: introduce GRAFANA_PUBLIC_URL var for client-facing MCP config
zeitlinger Mar 31, 2026
4393740
fix: restrict /etc/lgtm permissions, fix docs URL version prefix
zeitlinger Mar 31, 2026
e861ec2
fix: lint — shellcheck directive before source, split long line
zeitlinger Mar 31, 2026
871332e
fix: pipe at end of line for linter
zeitlinger Mar 31, 2026
2bc13fe
fix: unify pipeline overlay generation, drop static debug config
zeitlinger Apr 1, 2026
dd27848
test: add bats unit tests for run-otelcol.sh config generation
zeitlinger Apr 1, 2026
a7a212c
chore: remove unnecessary Renovate annotation from bats tool entry
zeitlinger Apr 1, 2026
5cc8f92
ci: run unit tests in lint workflow
zeitlinger Apr 1, 2026
67344d6
ci: add 'ci' task depending on lint and unit tests
zeitlinger Apr 1, 2026
96c5963
Merge branch 'main' into mcp-integration
zeitlinger Apr 1, 2026
fbd73ff
fix: shellcheck bats tests and update flint to v0.9.2
zeitlinger Apr 1, 2026
ae02003
fix: OATS brittleness, clarify MCP doc wording, add --init for signal…
zeitlinger Apr 1, 2026
a2ea1b7
fix: simplify docs version ref, add kubectl exec to README
zeitlinger Apr 2, 2026
6669518
fix: forward OTEL_COLLECTOR_DEBUG_EXPORTER to container; DRY up SA URL
zeitlinger Apr 8, 2026
01e8b8c
fix: add port 3200 to k8s port-forward; quote args in claude-mcp-setu…
zeitlinger Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/renovate-tracked-deps.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@
},
"mise.toml": {
"mise": [
"bats",
"go",
"go:github.com/grafana/oats",
"java",
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -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
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ...
Comment thread
zeitlinger marked this conversation as resolved.
# 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.
Comment thread
zeitlinger marked this conversation as resolved.

## Related Work

- [Metrics, Logs, Traces and Profiles in Grafana][mltp]
Expand All @@ -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/
Expand Down
73 changes: 73 additions & 0 deletions docker/run-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Comment thread
zeitlinger marked this conversation as resolved.

# Create a service account token and MCP config for AI tool access
Comment thread
zeitlinger marked this conversation as resolved.
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}"
Comment thread
zeitlinger marked this conversation as resolved.
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"
Comment thread
zeitlinger marked this conversation as resolved.
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
Expand All @@ -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)"
Comment thread
zeitlinger marked this conversation as resolved.
echo " - 4040: Pyroscope endpoint"
echo " - 9090: Prometheus endpoint"

Expand Down
146 changes: 146 additions & 0 deletions docker/run-otelcol.bats
Original file line number Diff line number Diff line change
@@ -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" <<EOF
run_with_logging() {
shift 2 # skip name and envvar args
printf '%s\n' "\$@" >"$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"
Comment thread
zeitlinger marked this conversation as resolved.
}

# --- 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"
Comment thread
zeitlinger marked this conversation as resolved.
}
Comment thread
zeitlinger marked this conversation as resolved.

@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"
Comment thread
zeitlinger marked this conversation as resolved.
}
Comment thread
zeitlinger marked this conversation as resolved.
Comment thread
zeitlinger marked this conversation as resolved.

# --- 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"
Comment thread
zeitlinger marked this conversation as resolved.
}
Comment thread
zeitlinger marked this conversation as resolved.

# --- 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"
Comment thread
zeitlinger marked this conversation as resolved.
}
Comment thread
zeitlinger marked this conversation as resolved.
Loading
Loading