This is an automated email from the ASF dual-hosted git repository.

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-mcp.git


The following commit(s) were added to refs/heads/main by this push:
     new 997a57b9 docs: correct docker-compose auto-start guidance and expand 
LGTM observability docs (#230)
997a57b9 is described below

commit 997a57b907a281707bdfa831680676ad42d22a38
Author: Aditya Parikh <[email protected]>
AuthorDate: Tue Oct 6 14:33:29 2026 -0400

    docs: correct docker-compose auto-start guidance and expand LGTM 
observability docs (#230)
    
    * docs(dev): correct docker-compose auto-start guidance for bootRun
    
    DEVELOPMENT.md and AGENTS.md told developers to run `docker compose up
    -d` before `bootRun`, then contradicted that a few lines later by
    saying Spring Boot Docker Compose starts the services automatically.
    
    The developmentOnly `spring-boot-docker-compose` /
    `spring-ai-spring-boot-docker-compose` starters mean `bootRun` really
    does auto-start (and stop) compose.yaml's `solr` and `zoo` services,
    so the manual step was redundant there. It's still required when
    running the packaged JAR, Docker image, or native image directly,
    since those artifacts exclude developmentOnly dependencies. The old
    text also claimed LGTM starts automatically, which is wrong: it's
    excluded via `org.springframework.boot.ignore: "true"` in
    compose.yaml and always needs a manual `docker compose up -d lgtm`.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
    Signed-off-by: Aditya Parikh <[email protected]>
    
    * docs(dev): scope docker-compose auto-start claim to HTTP mode only
    
    Code review on PR #230 caught that the previous commit's claim —
    "bootRun auto-starts compose.yaml" — was placed on the default STDIO
    path, but application-stdio.properties sets
    spring.docker.compose.enabled=false specifically to avoid startup
    delay and container-lifecycle noise on the JSON-RPC transport. Only
    application-http.properties enables it. Move the manual
    `docker compose up -d` step back to the STDIO path (and the JAR path,
    unchanged) and attach the auto-start explanation to HTTP mode only,
    where it is actually true.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
    Signed-off-by: Aditya Parikh <[email protected]>
    
    * docs: expand LGTM observability guidance with lifecycle notes and a 
walkthrough
    
    The observability docs described starting the LGTM stack with a plain
    docker compose up -d but didn't call out that lgtm carries
    org.springframework.boot.ignore: "true" in compose.yaml, so bootRun never
    starts or stops it in any transport mode (unlike solr/zoo in HTTP mode).
    Documents the manual docker compose up -d lgtm / stop lgtm commands and
    the in-memory-only storage.
    
    Also adds a runnable walkthrough (a curl against /mcp, or any MCP client)
    so there's something to look at, a "Pivoting Between the Three" section
    showing how a trace's "Logs for this span" link jumps to the correlated
    Loki lines, and a troubleshooting table for the common no-data cases.
    
    Co-Authored-By: Claude Sonnet 5 <[email protected]>
    Signed-off-by: Aditya Parikh <[email protected]>
    
    * docs(observability): describe the signals the server actually exports
    
    Measured against grafana/otel-lgtm, the page promised three things the
    http profile does not do:
    
    - Metrics: only the OpenTelemetry HTTP server metric
      (http_server_request_duration_seconds_*) is exported over OTLP. The
      PromQL examples queried Micrometer names (http_server_requests_seconds_*,
      jvm_memory_used_bytes), which exist only at /actuator/prometheus and
      return nothing in Grafana. Replace them with queries that return data.
    - Outgoing Solr calls are not traced as separate spans; their time is part
      of the tool span (e.g. search-service#search).
    - Trace-to-log linking works, but a successful tool call writes no log
      lines, so "Logs for this span" is empty for it. Add a failing
      check-health call whose WARN shows the link, a LogQL filter for
      request-scoped lines, and troubleshooting rows for both cases.
    
    Also note that OTEL_TRACES_URL sets the endpoint for all three signals and
    how to reach a host LGTM from a container, and that traces and metrics
    take up to a minute to appear.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Aditya Parikh <[email protected]>
    
    ---------
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Co-authored-by: Claude Sonnet 5 <[email protected]>
---
 AGENTS.md                                    |   9 +-
 dev-docs/DEVELOPMENT.md                      |  41 ++++++---
 docs/observability.md                        | 132 ++++++++++++++++++++++++---
 docs/site/content/pages/mcp/observability.md | 132 ++++++++++++++++++++++++---
 4 files changed, 273 insertions(+), 41 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index cbe4cd91..275d25f2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -43,8 +43,13 @@ Solr MCP Server is a Spring AI Model Context Protocol (MCP) 
server that enables
 ./gradlew nativeCompile -Pnative             # Compile native binary (host OS 
only)
 ./gradlew nativeTest -Pnative                # Run tests as native image
 
-# Run locally (requires `docker compose up -d` for Solr)
-./gradlew bootRun                  # STDIO mode (default)
+# Run locally
+# STDIO disables Docker Compose auto-start 
(spring.docker.compose.enabled=false in
+# application-stdio.properties) — run `docker compose up -d` first, same as 
for the
+# packaged jar/Docker/native image or to start the boot-ignored `lgtm` service.
+docker compose up -d && ./gradlew bootRun    # STDIO mode (default)
+# HTTP mode enables it (application-http.properties), so bootRun 
auto-starts/stops
+# compose.yaml's solr/zoo via the developmentOnly spring-boot-docker-compose 
starter.
 PROFILES=http ./gradlew bootRun    # HTTP mode
 ```
 
diff --git a/dev-docs/DEVELOPMENT.md b/dev-docs/DEVELOPMENT.md
index 88b5afa6..2bd6eb92 100644
--- a/dev-docs/DEVELOPMENT.md
+++ b/dev-docs/DEVELOPMENT.md
@@ -95,27 +95,28 @@ grype sbom:application.cdx.json
 
 ## Running Locally
 
-### Start Solr
+### Run the Server
+
+#### STDIO Mode (Default)
+
+`application-stdio.properties` sets `spring.docker.compose.enabled=false` 
(Docker Compose
+management is only useful for local HTTP development; in STDIO it would add 
startup delay and
+unexpected container lifecycle noise on the JSON-RPC transport), so start Solr 
by hand first:
 
 ```bash
 docker compose up -d
+./gradlew bootRun
 ```
 
-This starts a Solr instance in SolrCloud mode with ZooKeeper and creates two 
sample collections:
+This starts Solr in SolrCloud mode with ZooKeeper and creates two sample 
collections:
 - `books` - Created empty. The books.csv download and post are commented out in
   `init-solr.sh`, so use it as a scratch collection or uncomment those lines.
 - `films` - Collection populated with Solr's sample film data
 
-### Run the Server
-
-#### STDIO Mode (Default)
-
-```bash
-./gradlew bootRun
-```
-
-Or using the JAR:
+Or using the JAR — same manual step, since the `developmentOnly` 
docker-compose starter isn't on
+the packaged jar's runtime classpath either:
 ```bash
+docker compose up -d
 java -jar build/libs/solr-mcp-1.0.0-SNAPSHOT.jar
 ```
 
@@ -125,11 +126,25 @@ java -jar build/libs/solr-mcp-1.0.0-SNAPSHOT.jar
 PROFILES=http ./gradlew bootRun
 ```
 
-Spring Boot Docker Compose will automatically start the services declared in 
`compose.yaml`
-(Solr, ZooKeeper, and optionally LGTM for observability) before the 
application starts.
+`application-http.properties` sets `spring.docker.compose.enabled=true`, so 
this mode's
+`bootRun` picks up the `spring-boot-docker-compose` / 
`spring-ai-spring-boot-docker-compose`
+`developmentOnly` dependencies and auto-starts (then stops) the `solr` and 
`zoo` services
+declared in the root `compose.yaml` — no manual `docker compose up` needed 
here. Running the
+built JAR/Docker/native image in HTTP mode still needs the manual step, since 
those artifacts
+exclude `developmentOnly` dependencies.
 
 The server will start on http://localhost:8080
 
+#### Observability (optional)
+
+The `lgtm` service in `compose.yaml` carries `org.springframework.boot.ignore: 
"true"`, which
+opts it out of Spring Boot's Docker Compose lifecycle management. Start it by 
hand if you want
+the Grafana/Loki/Tempo/Mimir stack, regardless of run mode:
+
+```bash
+docker compose up -d lgtm
+```
+
 ### Environment Variables
 
 - `SOLR_URL`: Solr instance URL (default: `http://localhost:8983/solr/`)
diff --git a/docs/observability.md b/docs/observability.md
index b3c17671..42bdcb1c 100644
--- a/docs/observability.md
+++ b/docs/observability.md
@@ -6,11 +6,17 @@ When running in **HTTP mode**, the Solr MCP Server exports 
telemetry data via Op
 
 | Signal | Backend | What it shows |
 |--------|---------|---------------|
-| **Traces** | Tempo | Distributed traces for every MCP tool invocation, Solr 
query, and HTTP request |
-| **Metrics** | Mimir/Prometheus | JVM stats, HTTP request rates, Solr query 
latencies, cache hit ratios |
-| **Logs** | Loki | Structured application logs correlated with trace IDs |
+| **Traces** | Tempo | A trace per HTTP request, with a span for the MCP tool 
it invoked |
+| **Metrics** | Mimir/Prometheus | HTTP server request count and duration |
+| **Logs** | Loki | Application logs, each tagged with the trace and span it 
was written under |
 
-Every MCP tool invocation creates a trace span: search, indexing (JSON, CSV, 
XML), collection operations (list, stats, health, create), and schema 
retrieval. All incoming HTTP requests and outgoing Solr calls are automatically 
traced.
+Every MCP tool invocation creates a span named after the service and tool, 
such as
+`search-service#search` or `collection-service#check-health`, inside the trace 
of the HTTP
+request that carried it. The call to Solr is not a separate span; its time is 
part of the
+tool span.
+
+JVM, Tomcat and other Micrometer metrics are not exported over OTLP. They are 
served for
+scraping at `/actuator/prometheus` (see **Actuator Endpoints** below).
 
 ***
 
@@ -32,6 +38,28 @@ This starts:
 | OTLP gRPC | localhost:4317 | Trace/metric/log ingestion (gRPC) |
 | OTLP HTTP | localhost:4318 | Trace/metric/log ingestion (HTTP) |
 
+**LGTM is never auto-started.** The `lgtm` service carries
+`org.springframework.boot.ignore: "true"` in `compose.yaml`, which opts it out 
of Spring
+Boot's Docker Compose lifecycle management. This is independent of transport 
mode: even
+in HTTP mode, where `PROFILES=http ./gradlew bootRun` auto-starts (and stops) 
the `solr`
+and `zoo` services, LGTM is untouched. Start it explicitly, either with the
+`docker compose up -d` above (which starts everything) or, if `solr`/`zoo` are 
already
+running via `bootRun`'s auto-start, on its own:
+
+```bash
+docker compose up -d lgtm
+```
+
+Because it's Boot-ignored, `bootRun` won't stop it either&mdash;bring it down 
by hand
+when you're done:
+
+```bash
+docker compose stop lgtm
+```
+
+The `grafana/otel-lgtm` container stores everything in memory with no 
persistent volume,
+so a restart discards all traces, metrics, and logs.
+
 ### Run the Server with Observability ###
 
 ```bash
@@ -46,6 +74,46 @@ otel.exporter.otlp.endpoint=http://localhost:4317
 otel.exporter.otlp.protocol=grpc
 ```
 
+`otel.exporter.otlp.endpoint` is the endpoint for all three signals, so 
`OTEL_TRACES_URL`
+moves logs and metrics too, despite its name. When the server runs in a 
container and LGTM
+on the host, `localhost` is the container itself; set
+`OTEL_TRACES_URL=http://host.docker.internal:4317` (plus
+`--add-host=host.docker.internal:host-gateway` on Linux).
+
+### Generate Some Activity ###
+
+Grafana has nothing to show until at least one tool call runs. Any MCP client 
works
+(the [Quick Start](tutorial.md) prompts are enough), or call the HTTP endpoint 
directly.
+`HTTP_SECURITY_ENABLED=false` skips the OAuth2 setup for this local 
check&mdash;see
+[Security](security/http.md) to keep it on:
+
+```bash
+HTTP_SECURITY_ENABLED=false PROFILES=http ./gradlew bootRun
+```
+
+```bash
+curl -s -X POST http://localhost:8080/mcp \
+  -H "Content-Type: application/json" \
+  -H "Accept: application/json, text/event-stream" \
+  -d 
'{"jsonrpc":"2.0","method":"tools/call","id":1,"params":{"name":"list-collections","arguments":{}}}'
+```
+
+Run it a few times&mdash;each call is one trace and one more request in the 
metrics.
+
+A successful call writes no log lines, so it has nothing to show in Loki. To 
see a trace
+with a log attached, make one call that fails; a health check on a collection 
that does not
+exist logs a `WARN` inside the request:
+
+```bash
+curl -s -X POST http://localhost:8080/mcp \
+  -H "Content-Type: application/json" \
+  -H "Accept: application/json, text/event-stream" \
+  -d 
'{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"check-health","arguments":{"collection":"no-such-collection"}}}'
+```
+
+Traces take up to a minute to become searchable in Tempo, and metrics are 
exported once
+a minute, so an empty Grafana right after the calls is expected.
+
 ***
 
 ## Grafana ##
@@ -59,30 +127,53 @@ Open [http://localhost:3000](http://localhost:3000) and 
click **Explore** in the
 
         {.service.name="solr-mcp"}
 
-3. Click on a trace to see the span waterfall&mdash;each MCP tool invocation, 
Solr query, and HTTP request is a separate span
+3. Click on an `http post /mcp` trace to see the span waterfall: the security 
filter
+   chain, then one span for the tool (`search-service#search`,
+   `collection-service#check-health`, &hellip;) with its duration
 
 ### View Logs (Loki) ###
 
 1. Select **Loki** as the data source
 2. Use LogQL to search:
 
-        {service_name="solr-mcp"} |= "search"
+        {service_name="solr-mcp"} | trace_id != ""
 
-3. Logs are automatically correlated with trace IDs&mdash;click a log line to 
jump to its trace
+   That keeps only lines written during a request; drop the filter to include 
startup
+   and lifecycle lines, which have no trace.
+3. Expand a line and follow its **Trace** link to open the request in Tempo.
 
 ### View Metrics (Prometheus) ###
 
 1. Select **Prometheus** as the data source
 2. Example queries:
 
-        # HTTP request rate
-        rate(http_server_requests_seconds_count[5m])
-
-        # JVM memory usage
-        jvm_memory_used_bytes
+        # HTTP request rate, by method and status
+        sum by (http_request_method, http_response_status_code) 
(rate(http_server_request_duration_seconds_count{job="solr-mcp"}[5m]))
 
         # Request latency (p99)
-        histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m]))
+        histogram_quantile(0.99, sum by (le) 
(rate(http_server_request_duration_seconds_bucket{job="solr-mcp"}[5m])))
+
+   These are the OpenTelemetry HTTP server metrics, the only application 
metrics exported
+   over OTLP. Micrometer's names (`http_server_requests_seconds_*`, `jvm_*`) 
return nothing
+   here; read them from `/actuator/prometheus` instead.
+
+### Pivoting Between the Three ###
+
+The fastest way through all three signals for one request:
+
+1. Find the trace in **Tempo** (TraceQL query above) and open it.
+2. Each span carries a **Logs for this span** link&mdash;click it to jump to 
the
+   matching lines in **Loki**, filtered by trace ID. This works because the 
OTEL logback
+   appender (`logback-spring.xml`) tags every log line with the active trace 
and span ID.
+   The link filters on the trace, so it finds the same lines from any span in 
it.
+3. The link comes up empty for a trace that logged nothing, which is every 
successful
+   tool call: the services log only on failure. The `check-health` call above 
is the one
+   to follow; its `WARN` sits under the `collection-service#check-health` span.
+4. From a log line, the **Trace** link takes you back to its trace.
+5. **Metrics** aren't per-request the same way&mdash;there's no single 
span/log &harr;
+   metric-sample link&mdash;but the PromQL queries above will show the 
aggregate effect
+   (e.g. a rise in `http_server_request_duration_seconds_count`) of whatever 
activity you
+   just generated.
 
 ***
 
@@ -100,6 +191,21 @@ curl http://localhost:8080/actuator/loggers       # Logger 
levels
 
 ***
 
+## Troubleshooting ##
+
+| Symptom | Likely cause | Fix |
+|---------|-------------|-----|
+| No traces/metrics/logs show up in Grafana at all | LGTM was never 
started&mdash;`bootRun` does not start it in either mode | `docker compose up 
-d lgtm` |
+| Tempo finds nothing right after the calls | Traces take up to a minute to 
become searchable; metrics are exported once a minute | Wait and re-run the 
query |
+| **Logs for this span** is empty | The request logged nothing; successful 
tool calls never do | Expected; follow a failing call such as `check-health` on 
a missing collection |
+| `jvm_*` or `http_server_requests_seconds_*` returns nothing in Grafana | 
Those are Micrometer metrics, served only at `/actuator/prometheus` | Query 
`http_server_request_duration_seconds_*` in Grafana, or curl the actuator |
+| Traces appear but stop after a restart | The `otel-lgtm` container has no 
persistent volume | Expected; re-run your workload after restarting `lgtm` |
+| `otel.exporter.otlp.endpoint` connection refused | Running the server 
outside the `search` Docker network (e.g. inside its own container) while LGTM 
is on the host | Point `OTEL_TRACES_URL` at a reachable host, or join the same 
Docker network |
+| Traces are sparse or missing under load | 
`management.tracing.sampling.probability` is below 1.0 | Raise it for the 
session you're debugging; keep it low in production |
+| No data in **STDIO** mode | Tracing/metrics export is an HTTP-mode 
feature&mdash;STDIO has no servlet layer to instrument | Run `PROFILES=http 
./gradlew bootRun` instead |
+
+***
+
 ## Production Configuration ##
 
 For production, reduce the sampling rate and configure the OTLP endpoint for 
your collector:
diff --git a/docs/site/content/pages/mcp/observability.md 
b/docs/site/content/pages/mcp/observability.md
index 46da40be..ba8015f2 100644
--- a/docs/site/content/pages/mcp/observability.md
+++ b/docs/site/content/pages/mcp/observability.md
@@ -9,11 +9,17 @@ When running in **HTTP mode**, the Solr MCP Server exports 
telemetry data via Op
 
 | Signal | Backend | What it shows |
 |--------|---------|---------------|
-| **Traces** | Tempo | Distributed traces for every MCP tool invocation, Solr 
query, and HTTP request |
-| **Metrics** | Mimir/Prometheus | JVM stats, HTTP request rates, Solr query 
latencies, cache hit ratios |
-| **Logs** | Loki | Structured application logs correlated with trace IDs |
+| **Traces** | Tempo | A trace per HTTP request, with a span for the MCP tool 
it invoked |
+| **Metrics** | Mimir/Prometheus | HTTP server request count and duration |
+| **Logs** | Loki | Application logs, each tagged with the trace and span it 
was written under |
 
-Every MCP tool invocation creates a trace span: search, indexing (JSON, CSV, 
XML), collection operations (list, stats, health, create), and schema 
retrieval. All incoming HTTP requests and outgoing Solr calls are automatically 
traced.
+Every MCP tool invocation creates a span named after the service and tool, 
such as
+`search-service#search` or `collection-service#check-health`, inside the trace 
of the HTTP
+request that carried it. The call to Solr is not a separate span; its time is 
part of the
+tool span.
+
+JVM, Tomcat and other Micrometer metrics are not exported over OTLP. They are 
served for
+scraping at `/actuator/prometheus` (see **Actuator Endpoints** below).
 
 ***
 
@@ -35,6 +41,28 @@ This starts:
 | OTLP gRPC | localhost:4317 | Trace/metric/log ingestion (gRPC) |
 | OTLP HTTP | localhost:4318 | Trace/metric/log ingestion (HTTP) |
 
+**LGTM is never auto-started.** The `lgtm` service carries
+`org.springframework.boot.ignore: "true"` in `compose.yaml`, which opts it out 
of Spring
+Boot's Docker Compose lifecycle management. This is independent of transport 
mode: even
+in HTTP mode, where `PROFILES=http ./gradlew bootRun` auto-starts (and stops) 
the `solr`
+and `zoo` services, LGTM is untouched. Start it explicitly, either with the
+`docker compose up -d` above (which starts everything) or, if `solr`/`zoo` are 
already
+running via `bootRun`'s auto-start, on its own:
+
+```bash
+docker compose up -d lgtm
+```
+
+Because it's Boot-ignored, `bootRun` won't stop it either&mdash;bring it down 
by hand
+when you're done:
+
+```bash
+docker compose stop lgtm
+```
+
+The `grafana/otel-lgtm` container stores everything in memory with no 
persistent
+volume, so a restart discards all traces, metrics, and logs.
+
 ### Run the Server with Observability ###
 
 ```bash
@@ -49,6 +77,46 @@ otel.exporter.otlp.endpoint=http://localhost:4317
 otel.exporter.otlp.protocol=grpc
 ```
 
+`otel.exporter.otlp.endpoint` is the endpoint for all three signals, so 
`OTEL_TRACES_URL`
+moves logs and metrics too, despite its name. When the server runs in a 
container and LGTM
+on the host, `localhost` is the container itself; set
+`OTEL_TRACES_URL=http://host.docker.internal:4317` (plus
+`--add-host=host.docker.internal:host-gateway` on Linux).
+
+### Generate Some Activity ###
+
+Grafana has nothing to show until at least one tool call runs. Any MCP client 
works
+(the [Quick Start](/mcp/quick-start.html) prompts are enough), or call the HTTP
+endpoint directly. `HTTP_SECURITY_ENABLED=false` skips the OAuth2 setup for 
this local
+check&mdash;see [Security](/mcp/security.html) to keep it on:
+
+```bash
+HTTP_SECURITY_ENABLED=false PROFILES=http ./gradlew bootRun
+```
+
+```bash
+curl -s -X POST http://localhost:8080/mcp \
+  -H "Content-Type: application/json" \
+  -H "Accept: application/json, text/event-stream" \
+  -d 
'{"jsonrpc":"2.0","method":"tools/call","id":1,"params":{"name":"list-collections","arguments":{}}}'
+```
+
+Run it a few times&mdash;each call is one trace and one more request in the 
metrics.
+
+A successful call writes no log lines, so it has nothing to show in Loki. To 
see a trace
+with a log attached, make one call that fails; a health check on a collection 
that does not
+exist logs a `WARN` inside the request:
+
+```bash
+curl -s -X POST http://localhost:8080/mcp \
+  -H "Content-Type: application/json" \
+  -H "Accept: application/json, text/event-stream" \
+  -d 
'{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"check-health","arguments":{"collection":"no-such-collection"}}}'
+```
+
+Traces take up to a minute to become searchable in Tempo, and metrics are 
exported once
+a minute, so an empty Grafana right after the calls is expected.
+
 ***
 
 ## Grafana ##
@@ -62,30 +130,53 @@ Open [http://localhost:3000](http://localhost:3000) and 
click **Explore** in the
 
         {.service.name="solr-mcp"}
 
-3. Click on a trace to see the span waterfall&mdash;each MCP tool invocation, 
Solr query, and HTTP request is a separate span
+3. Click on an `http post /mcp` trace to see the span waterfall: the security 
filter
+   chain, then one span for the tool (`search-service#search`,
+   `collection-service#check-health`, &hellip;) with its duration
 
 ### View Logs (Loki) ###
 
 1. Select **Loki** as the data source
 2. Use LogQL to search:
 
-        {service_name="solr-mcp"} |= "search"
+        {service_name="solr-mcp"} | trace_id != ""
 
-3. Logs are automatically correlated with trace IDs&mdash;click a log line to 
jump to its trace
+   That keeps only lines written during a request; drop the filter to include 
startup
+   and lifecycle lines, which have no trace.
+3. Expand a line and follow its **Trace** link to open the request in Tempo.
 
 ### View Metrics (Prometheus) ###
 
 1. Select **Prometheus** as the data source
 2. Example queries:
 
-        # HTTP request rate
-        rate(http_server_requests_seconds_count[5m])
-
-        # JVM memory usage
-        jvm_memory_used_bytes
+        # HTTP request rate, by method and status
+        sum by (http_request_method, http_response_status_code) 
(rate(http_server_request_duration_seconds_count{job="solr-mcp"}[5m]))
 
         # Request latency (p99)
-        histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m]))
+        histogram_quantile(0.99, sum by (le) 
(rate(http_server_request_duration_seconds_bucket{job="solr-mcp"}[5m])))
+
+   These are the OpenTelemetry HTTP server metrics, the only application 
metrics exported
+   over OTLP. Micrometer's names (`http_server_requests_seconds_*`, `jvm_*`) 
return nothing
+   here; read them from `/actuator/prometheus` instead.
+
+### Pivoting Between the Three ###
+
+The fastest way through all three signals for one request:
+
+1. Find the trace in **Tempo** (TraceQL query above) and open it.
+2. Each span carries a **Logs for this span** link&mdash;click it to jump to 
the
+   matching lines in **Loki**, filtered by trace ID. This works because the 
OTEL logback
+   appender (`logback-spring.xml`) tags every log line with the active trace 
and span ID.
+   The link filters on the trace, so it finds the same lines from any span in 
it.
+3. The link comes up empty for a trace that logged nothing, which is every 
successful
+   tool call: the services log only on failure. The `check-health` call above 
is the one
+   to follow; its `WARN` sits under the `collection-service#check-health` span.
+4. From a log line, the **Trace** link takes you back to its trace.
+5. **Metrics** aren't per-request the same way&mdash;there's no single 
span/log &harr;
+   metric-sample link&mdash;but the PromQL queries above will show the 
aggregate effect
+   (e.g. a rise in `http_server_request_duration_seconds_count`) of whatever 
activity you
+   just generated.
 
 ***
 
@@ -103,6 +194,21 @@ curl http://localhost:8080/actuator/loggers       # Logger 
levels
 
 ***
 
+## Troubleshooting ##
+
+| Symptom | Likely cause | Fix |
+|---------|-------------|-----|
+| No traces/metrics/logs show up in Grafana at all | LGTM was never 
started&mdash;`bootRun` does not start it in either mode | `docker compose up 
-d lgtm` |
+| Tempo finds nothing right after the calls | Traces take up to a minute to 
become searchable; metrics are exported once a minute | Wait and re-run the 
query |
+| **Logs for this span** is empty | The request logged nothing; successful 
tool calls never do | Expected; follow a failing call such as `check-health` on 
a missing collection |
+| `jvm_*` or `http_server_requests_seconds_*` returns nothing in Grafana | 
Those are Micrometer metrics, served only at `/actuator/prometheus` | Query 
`http_server_request_duration_seconds_*` in Grafana, or curl the actuator |
+| Traces appear but stop after a restart | The `otel-lgtm` container has no 
persistent volume | Expected; re-run your workload after restarting `lgtm` |
+| `otel.exporter.otlp.endpoint` connection refused | Running the server 
outside the `search` Docker network (e.g. inside its own container) while LGTM 
is on the host | Point `OTEL_TRACES_URL` at a reachable host, or join the same 
Docker network |
+| Traces are sparse or missing under load | 
`management.tracing.sampling.probability` is below 1.0 | Raise it for the 
session you're debugging; keep it low in production |
+| No data in **STDIO** mode | Tracing/metrics export is an HTTP-mode 
feature&mdash;STDIO has no servlet layer to instrument | Run `PROFILES=http 
./gradlew bootRun` instead |
+
+***
+
 ## Production Configuration ##
 
 For production, reduce the sampling rate and configure the OTLP endpoint for 
your collector:

Reply via email to