diff --git a/.gitignore b/.gitignore index c6dd127..af694eb 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ out/ .env .auth-token *.token + +### Git worktrees ### +.worktrees/ diff --git a/README.md b/README.md index 488254e..f05da0f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A Spring AI Model Context Protocol (MCP) server that provides tools for interact - πŸ”§ Inspect schema - πŸ”Œ Transports: STDIO (Claude Desktop) and HTTP (MCP Inspector) - πŸ” OAuth2 security with Auth0 (HTTP mode only) +- πŸ“ˆ OpenTelemetry observability: metrics, traces, logs (HTTP mode only) - 🐳 Docker images built with Jib ## Get started (users) @@ -379,6 +380,7 @@ The `solr://{collection}/schema` resource supports autocompletion for the `{coll ## Documentation - [Auth0 Setup (OAuth2 configuration)](docs/AUTH0_SETUP.md) +- [Observability Guide (metrics, traces, logs)](dev-docs/Observability.md) ## Contributing diff --git a/arconia-cli.log b/arconia-cli.log new file mode 100644 index 0000000..e69de29 diff --git a/build.gradle.kts b/build.gradle.kts index 201bc32..566f8a8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -87,13 +87,18 @@ configurations { repositories { mavenCentral() + maven { url = uri("https://repo.spring.io/milestone") } } dependencies { - developmentOnly(libs.bundles.spring.boot.dev) + developmentOnly(libs.spring.boot.docker.compose) + developmentOnly(libs.spring.ai.spring.boot.docker.compose) { + exclude(group = "org.springframework.boot", module = "spring-boot-starter-mongodb") + } - implementation(libs.spring.boot.starter.web) + implementation(libs.spring.boot.starter.webmvc) + implementation(libs.spring.boot.starter.json) implementation(libs.spring.boot.starter.actuator) implementation(libs.spring.boot.starter.aop) implementation(libs.spring.ai.starter.mcp.server.webmvc) @@ -101,8 +106,6 @@ dependencies { exclude(group = "org.apache.httpcomponents") } implementation(libs.commons.csv) - // JSpecify for nullability annotations - implementation(libs.jspecify) implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.11.0")) implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter") @@ -115,6 +118,15 @@ dependencies { implementation(libs.spring.boot.starter.security) implementation(libs.spring.boot.starter.oauth2.resource.server) + // OpenTelemetry (HTTP mode only - for metrics, tracing, and log export) + implementation(libs.spring.boot.starter.opentelemetry) + implementation(libs.opentelemetry.logback.appender) + implementation("io.micrometer:micrometer-tracing-bridge-otel") + runtimeOnly(libs.micrometer.registry.otlp) + + // AspectJ (required for @Observed annotation support in Spring Boot 4) + implementation(libs.spring.boot.starter.aspectj) + // Error Prone and NullAway for null safety analysis errorprone(libs.errorprone.core) errorprone(libs.nullaway) @@ -126,8 +138,18 @@ dependencies { dependencyManagement { imports { mavenBom("org.springframework.ai:spring-ai-bom:${libs.versions.spring.ai.get()}") - // Align Jetty family to 10.x compatible with SolrJ 9.x - mavenBom("org.eclipse.jetty:jetty-bom:${libs.versions.jetty.get()}") + } +} + +// Force opentelemetry-proto to a version compiled with protobuf 3.x +// This resolves NoSuchMethodError with protobuf 4.x +// See: https://github.com/micrometer-metrics/micrometer/issues/5658 +configurations.all { + resolutionStrategy.eachDependency { + if (requested.group == "io.opentelemetry.proto" && requested.name == "opentelemetry-proto") { + useVersion("1.3.2-alpha") + because("Version 1.8.0-alpha has protobuf 4.x incompatibility causing NoSuchMethodError") + } } } @@ -189,6 +211,34 @@ tasks.jacocoTestReport { ) } +tasks.jacocoTestCoverageVerification { + dependsOn(tasks.jacocoTestReport) + violationRules { + rule { + limit { + minimum = "0.50".toBigDecimal() + } + } + } + // Use same class directory exclusions as the report + classDirectories.setFrom( + files( + classDirectories.files.map { + fileTree(it) { + exclude( + "**/DockerImageStdioIntegrationTest*.class", + "**/DockerImageHttpIntegrationTest*.class", + ) + } + }, + ), + ) +} + +tasks.check { + dependsOn(tasks.jacocoTestCoverageVerification) +} + tasks.withType().configureEach { options.errorprone { disableAllChecks.set(true) // Other error prone checks are disabled diff --git a/compose.yaml b/compose.yaml index 75c5920..dd8d577 100644 --- a/compose.yaml +++ b/compose.yaml @@ -35,27 +35,33 @@ services: environment: ZOO_4LW_COMMANDS_WHITELIST: "mntr,conf,ruok" - # ============================================================================= - # LGTM Stack - Grafana observability backend (Loki, Grafana, Tempo, Mimir) - # ============================================================================= - # This all-in-one container provides: - # - Loki: Log aggregation (LogQL queries) - # - Grafana: Visualization at http://localhost:3000 (no auth required) - # - Tempo: Distributed tracing (TraceQL queries) - # - Mimir: Prometheus-compatible metrics storage - # - OpenTelemetry Collector: Receives OTLP data on ports 4317 (gRPC) and 4318 (HTTP) - # - # Spring Boot auto-configures OTLP endpoints when this container is running. + # ============================================================================= + # OpenTelemetry LGTM Stack (HTTP mode only) + # ============================================================================= + # Provides a complete observability stack for local development: + # - Grafana: Visualization dashboards (http://localhost:3000) + # - Loki: Log aggregation + # - Tempo: Distributed tracing + # - Mimir: Metrics storage (Prometheus-compatible) + # - OpenTelemetry Collector: Receives OTLP data on ports 4317 (gRPC) and 4318 (HTTP) + # + # Usage: + # docker compose up -d lgtm # Start only the observability stack + # docker compose up -d # Start everything including Solr + # + # Access Grafana at http://localhost:3000 (no authentication required) + # Pre-configured datasources for Loki, Tempo, and Mimir are available. lgtm: - image: grafana/otel-lgtm:latest - ports: - - "3000:3000" # Grafana UI - - "4317:4317" # OTLP gRPC receiver - - "4318:4318" # OTLP HTTP receiver - networks: [ search ] - labels: - # Prevent Spring Boot auto-configuration from trying to manage this service - org.springframework.boot.ignore: "true" + image: grafana/otel-lgtm:latest + ports: + - "3000:3000" # Grafana UI + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + networks: [ search ] + environment: + # Disable authentication for local development + GF_AUTH_ANONYMOUS_ENABLED: "true" + GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin" volumes: data: diff --git a/dev-docs/Observability.md b/dev-docs/Observability.md new file mode 100644 index 0000000..6f778fa --- /dev/null +++ b/dev-docs/Observability.md @@ -0,0 +1,314 @@ +# Observability Guide for Solr MCP Server + +This guide covers setting up observability (metrics, traces, and logs) for the Solr MCP Server running in HTTP mode using OpenTelemetry. + +## Table of Contents + +- [Overview](#overview) +- [The LGTM Stack](#the-lgtm-stack) +- [Quick Start](#quick-start) +- [Architecture](#architecture) +- [Accessing Telemetry Data](#accessing-telemetry-data) + - [Grafana Dashboard](#grafana-dashboard) + - [Viewing Traces](#viewing-traces) + - [Viewing Logs](#viewing-logs) + - [Viewing Metrics](#viewing-metrics) +- [Configuration](#configuration) + - [Environment Variables](#environment-variables) + - [Sampling Configuration](#sampling-configuration) + - [Custom OTLP Endpoints](#custom-otlp-endpoints) +- [Production Considerations](#production-considerations) +- [Troubleshooting](#troubleshooting) + +## Overview + +The Solr MCP Server integrates with OpenTelemetry to provide comprehensive observability in HTTP mode: + +| Signal | Description | Backend | +|--------|-------------|---------| +| **Traces** | Distributed tracing for request flows | Tempo | +| **Metrics** | Application and JVM metrics | Mimir (Prometheus-compatible) | +| **Logs** | Structured log export with trace correlation | Loki | + +**Note:** Observability is only available in HTTP mode. STDIO mode disables telemetry to prevent stdout pollution that would interfere with MCP protocol communication. + +## The LGTM Stack + +The project uses the **Grafana LGTM stack** (`grafana/otel-lgtm`) - an all-in-one Docker image that provides a complete observability backend for local development. LGTM stands for: + +| Component | Purpose | Port | +|-----------|---------|------| +| **L**oki | Log aggregation and querying | Internal | +| **G**rafana | Visualization, dashboards, and exploration | 3000 | +| **T**empo | Distributed tracing backend | Internal | +| **M**imir | Prometheus-compatible metrics storage | Internal | + +The image also includes an **OpenTelemetry Collector** that receives telemetry data via OTLP protocol: +- **Port 4317**: OTLP gRPC receiver +- **Port 4318**: OTLP HTTP receiver (used by Spring Boot) + +This single container replaces what would otherwise require deploying and configuring multiple services separately, making it ideal for local development and testing. + +## Quick Start + +Thanks to the `spring-boot-docker-compose` dependency, **Docker containers are automatically started** when you run the application locally. Simply run: + +```bash +# Run the MCP server in HTTP mode - Docker containers start automatically! +PROFILES=http ./gradlew bootRun +``` + +Spring Boot detects the `compose.yaml` file and automatically: +1. Starts the `lgtm` container (Grafana, Loki, Tempo, Mimir) +2. Starts the `solr` and `zoo` containers +3. Configures OTLP endpoints to point to the running containers +4. Waits for containers to be healthy before accepting requests + +Once running, open Grafana at **http://localhost:3000** to explore your telemetry data. + +**Note:** To start containers manually (e.g., for debugging), use: +```bash +docker compose up -d lgtm solr +``` + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” OTLP/HTTP β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Solr MCP Server │─────────────────────│ OpenTelemetry Collector β”‚ +β”‚ (HTTP mode) β”‚ :4318 β”‚ (grafana/otel-lgtm) β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Traces │──┼─────────────────────┼─▢│ Tempo β”‚ β”‚ Grafana β”‚ β”‚ +β”‚ β”‚ (auto-instr.) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ :3000 β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - Dashboardsβ”‚ β”‚ +β”‚ β”‚ Metrics │──┼─────────────────────┼─▢│ Mimir β”‚ β”‚ - Explore β”‚ β”‚ +β”‚ β”‚ (actuator) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - Alerts β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Logs │──┼─────────────────────┼─▢│ Loki β”‚ β”‚ +β”‚ β”‚ (logback) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Accessing Telemetry Data + +### Grafana Dashboard + +Access Grafana at **http://localhost:3000** (no login required in development mode). + +The LGTM stack comes with pre-configured datasources: +- **Tempo** - For distributed traces +- **Loki** - For logs +- **Mimir** - For metrics (Prometheus-compatible) + +### Viewing Traces + +Grafana's **Drilldown** feature provides an integrated view for exploring traces, metrics, and logs all in one place. + +1. Open Grafana: http://localhost:3000 +2. Go to **Drilldown** > **Traces** in the sidebar +3. Select **Tempo** as the datasource +4. Filter traces by: + - Service name: `solr-mcp-server` + - Span name (e.g., `http post /mcp`) + - Duration + - URL path + +The trace view shows the complete request flow with timing breakdown for each span: + +![Distributed Tracing in Grafana](images/grafana-traces.png) + +In this example, you can see: +- The root span `http post /mcp` taking 223.98ms total +- Security filter chain spans for authentication/authorization +- The `SearchService#search` span (177.01ms) created by the `@Observed` annotation on the service method +- Nested security filter spans for the secured request + +**Navigating Between Signals:** + +The Drilldown sidebar provides quick access to related telemetry: +- **Metrics** - View application and JVM metrics (request rates, latencies, memory usage) +- **Logs** - View correlated logs with the same trace ID +- **Traces** - The current distributed trace view +- **Profiles** - CPU and memory profiling data (if configured) + +This unified view makes it easy to investigate issues by correlating traces with their associated logs and metrics. + +**Example TraceQL query:** +``` +{resource.service.name="solr-mcp-server"} +``` + +### Viewing Logs + +1. Open Grafana: http://localhost:3000 +2. Go to **Explore** +3. Select **Loki** as the datasource +4. Query logs using LogQL: + +**Example queries:** +```logql +# All logs from the MCP server +{service_name="solr-mcp-server"} + +# Error logs only +{service_name="solr-mcp-server"} |= "ERROR" + +# Logs with specific trace ID +{service_name="solr-mcp-server"} | json | trace_id="" +``` + +### Viewing Metrics + +1. Open Grafana: http://localhost:3000 +2. Go to **Explore** +3. Select **Mimir** as the datasource +4. Query metrics using PromQL: + +**Example queries:** +```promql +# HTTP request rate +rate(http_server_requests_seconds_count{application="solr-mcp-server"}[5m]) + +# Request latency (p99) +histogram_quantile(0.99, rate(http_server_requests_seconds_bucket{application="solr-mcp-server"}[5m])) + +# JVM memory usage +jvm_memory_used_bytes{application="solr-mcp-server"} + +# Active threads +jvm_threads_live_threads{application="solr-mcp-server"} +``` + +## Configuration + +### Environment Variables + +For production deployments without Docker Compose, set these environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `OTEL_SAMPLING_PROBABILITY` | `1.0` | Trace sampling rate (0.0-1.0) | +| `OTEL_METRICS_URL` | (auto-configured) | OTLP metrics endpoint | +| `OTEL_TRACES_URL` | (auto-configured) | OTLP traces endpoint | +| `OTEL_LOGS_URL` | (auto-configured) | OTLP logs endpoint | + +Example production configuration: +```bash +export OTEL_SAMPLING_PROBABILITY=0.1 +export OTEL_METRICS_URL=https://otel-collector.prod.example.com/v1/metrics +export OTEL_TRACES_URL=https://otel-collector.prod.example.com/v1/traces +export OTEL_LOGS_URL=https://otel-collector.prod.example.com/v1/logs +``` + +### Sampling Configuration + +For production, reduce sampling to manage costs and storage: + +```bash +# Sample 10% of traces +export OTEL_SAMPLING_PROBABILITY=0.1 +``` + +Or in `application-http.properties`: +```properties +management.tracing.sampling.probability=0.1 +``` + +### Custom OTLP Endpoints + +To send telemetry to a different backend (e.g., Jaeger, Datadog, New Relic): + +```bash +# Example: Send traces to Jaeger +export OTEL_TRACES_URL=http://jaeger:4318/v1/traces + +# Example: Send metrics to Prometheus remote write endpoint +export OTEL_METRICS_URL=http://prometheus:9090/api/v1/otlp/v1/metrics +``` + +## Production Considerations + +### 1. Use Secure Endpoints + +```properties +# Use HTTPS for production OTLP endpoints +management.otlp.metrics.export.url=https://otel-collector.prod.example.com/v1/metrics +management.opentelemetry.tracing.export.otlp.endpoint=https://otel-collector.prod.example.com/v1/traces +management.opentelemetry.logging.export.otlp.endpoint=https://otel-collector.prod.example.com/v1/logs +``` + +### 2. Add Authentication Headers + +If your OTLP collector requires authentication, configure headers in your OpenTelemetry configuration. + +### 3. Resource Attributes + +Add deployment-specific attributes for better filtering: + +```properties +spring.application.name=solr-mcp-server-prod +``` + +## Troubleshooting + +### No Data in Grafana + +1. **Check the LGTM container is running:** + ```bash + docker compose ps lgtm + ``` + +2. **Verify OTLP endpoints are reachable:** + ```bash + curl -v http://localhost:4318/v1/traces + ``` + +3. **Check application logs for OTLP errors:** + ```bash + ./gradlew bootRun 2>&1 | grep -i otel + ``` + +### Traces Not Appearing + +1. Ensure you're running in HTTP mode (`PROFILES=http`) +2. Check sampling probability is > 0 +3. Verify the trace endpoint URL is correct + +### Logs Not Appearing + +1. Check that logback-spring.xml is being loaded +2. Verify the OTEL appender is installed (check startup logs) +3. Ensure log level is INFO or lower + +### Metrics Not Appearing + +1. Verify actuator endpoints are exposed: + ```bash + curl http://localhost:8080/actuator/metrics + ``` +2. Check the metrics endpoint URL is correct + +### High Memory Usage + +If the LGTM container uses too much memory: +```yaml +# compose.yaml +lgtm: + image: grafana/otel-lgtm:latest + deploy: + resources: + limits: + memory: 2G +``` + +## References + +- [Spring Boot OpenTelemetry](https://docs.spring.io/spring-boot/reference/actuator/tracing.html) +- [OpenTelemetry Documentation](https://opentelemetry.io/docs/) +- [Grafana LGTM Stack](https://grafana.com/blog/2024/03/13/an-opentelemetry-backend-in-a-docker-image-introducing-grafana/otel-lgtm/) +- [LogQL Query Language](https://grafana.com/docs/loki/latest/logql/) +- [TraceQL Query Language](https://grafana.com/docs/tempo/latest/traceql/) diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..89f9ccc --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.jvmargs=-Xmx2048m diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4463b7..0c851ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,39 +1,42 @@ [versions] # Build plugins -spring-boot = "3.5.8" +spring-boot = "4.0.2" spring-dependency-management = "1.1.7" errorprone-plugin = "4.2.0" jib = "3.4.5" spotless = "7.0.2" # Main dependencies -spring-ai = "1.1.2" +spring-ai = "2.0.0-M2" solr = "9.9.0" commons-csv = "1.10.0" -jspecify = "1.0.0" -mcp-server-security = "0.0.4" +mcp-server-security = "0.0.6" + +# OpenTelemetry +opentelemetry-logback-appender = "2.21.0-alpha" # Error Prone and analysis tools errorprone-core = "2.38.0" nullaway = "0.12.7" -# Jetty BOM version -jetty = "10.0.22" - # Test dependencies -testcontainers = "1.21.3" +testcontainers = "2.0.2" awaitility = "4.2.2" opentelemetry-instrumentation-bom = "2.11.0" [libraries] # Spring -spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } +spring-boot-starter-webmvc = { module = "org.springframework.boot:spring-boot-starter-webmvc" } +spring-boot-starter-json = { module = "org.springframework.boot:spring-boot-starter-json" } spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator" } spring-boot-starter-aop = { module = "org.springframework.boot:spring-boot-starter-aop" } spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" } spring-boot-starter-oauth2-resource-server = { module = "org.springframework.boot:spring-boot-starter-oauth2-resource-server" } spring-boot-docker-compose = { module = "org.springframework.boot:spring-boot-docker-compose" } spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" } +spring-boot-starter-actuator-test = { module = "org.springframework.boot:spring-boot-starter-actuator-test" } +spring-boot-starter-opentelemetry-test = { module = "org.springframework.boot:spring-boot-starter-opentelemetry-test" } +spring-boot-starter-webmvc-test = { module = "org.springframework.boot:spring-boot-starter-webmvc-test" } spring-boot-testcontainers = { module = "org.springframework.boot:spring-boot-testcontainers" } # Spring AI spring-ai-starter-mcp-server-webmvc = { module = "org.springframework.ai:spring-ai-starter-mcp-server-webmvc" } @@ -50,8 +53,13 @@ solr-solrj = { module = "org.apache.solr:solr-solrj", version.ref = "solr" } # Apache Commons commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commons-csv" } -# Null safety -jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" } +# OpenTelemetry (HTTP mode only) +spring-boot-starter-opentelemetry = { module = "org.springframework.boot:spring-boot-starter-opentelemetry" } +opentelemetry-logback-appender = { module = "io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0", version.ref = "opentelemetry-logback-appender" } +micrometer-registry-otlp = { module = "io.micrometer:micrometer-registry-otlp" } + +# AspectJ (required for @Observed annotation support) +spring-boot-starter-aspectj = { module = "org.springframework.boot:spring-boot-starter-aspectj" } # Error Prone errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone-core" } @@ -61,9 +69,9 @@ nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway" } micrometer-tracing-bridge-otel = { module = "io.micrometer:micrometer-tracing-bridge-otel" } # Test dependencies -testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter" } -testcontainers-solr = { module = "org.testcontainers:solr", version.ref = "testcontainers" } -testcontainers-grafana = { module = "org.testcontainers:grafana", version.ref = "testcontainers" } +testcontainers-junit-jupiter = { module = "org.testcontainers:testcontainers-junit-jupiter", version.ref = "testcontainers" } +testcontainers-solr = { module = "org.testcontainers:testcontainers-solr", version.ref = "testcontainers" } +testcontainers-grafana = { module = "org.testcontainers:testcontainers-grafana", version.ref = "testcontainers" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" } opentelemetry-sdk-testing = { module = "io.opentelemetry:opentelemetry-sdk-testing" } @@ -75,7 +83,7 @@ jetty-util = { module = "org.eclipse.jetty:jetty-util" } [bundles] spring-ai-mcp = [ - "spring-boot-starter-web", + "spring-boot-starter-webmvc", "spring-ai-starter-mcp-server-webmvc" ] @@ -86,6 +94,9 @@ spring-boot-dev = [ test = [ "spring-boot-starter-test", + "spring-boot-starter-actuator-test", + "spring-boot-starter-opentelemetry-test", + "spring-boot-starter-webmvc-test", "spring-boot-testcontainers", "spring-ai-spring-boot-testcontainers", "testcontainers-junit-jupiter", diff --git a/images/grafana-traces.png b/images/grafana-traces.png new file mode 100644 index 0000000..f542792 Binary files /dev/null and b/images/grafana-traces.png differ diff --git a/settings.gradle.kts b/settings.gradle.kts index 8373d94..317049b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,18 @@ */ rootProject.name = "solr-mcp" + +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + maven { url = uri("https://repo.spring.io/milestone") } + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + maven { url = uri("https://repo.spring.io/milestone") } + } +} diff --git a/solr-mcp-tutorial.html b/solr-mcp-tutorial.html new file mode 100644 index 0000000..cea07d4 --- /dev/null +++ b/solr-mcp-tutorial.html @@ -0,0 +1,817 @@ + + + + + + Solr MCP Server - Usage Tutorial + + + +

Solr MCP Server - Usage Tutorial

+ +

The Solr MCP (Model Context Protocol) Server enables AI assistants like Claude to interact with Apache Solr through a + standardized protocol. This guide provides comprehensive instructions for setting up, configuring, and using the + Solr MCP Server.

+ + + +

Overview

+ +

The Solr MCP Server is a Spring AI-based implementation that provides AI assistants with tools to interact with + Apache Solr. It supports both STDIO and HTTP transport modes, enabling flexible deployment options.

+ +

What's Inside

+ + +
+ Model Context Protocol (MCP)
+ MCP is an open protocol that standardizes how applications provide context to LLMs. The Solr MCP Server implements + this protocol to make Solr's capabilities accessible to AI assistants. +
+ +

Prerequisites

+ + + +
+ Quick Start with Sample Data: The repository includes a Docker Compose file to start Solr with + pre-populated collections (books, films, techproducts). Run: docker compose up -d +
+ +

Integration with MCP Clients

+ +

The Solr MCP Server can be integrated with any MCP-compatible client. Both STDIO mode and HTTP + mode are fully supported.

+ +

STDIO Mode

+ +

STDIO mode uses standard input/output for communication. This is the default mode and works with Claude Desktop, + GitHub Copilot, VSCode extensions, and other MCP clients.

+ +

Claude Desktop

+ +

Edit your configuration file:

+ + +

Using Docker:

+
{
+  "mcpServers": {
+    "solr-mcp": {
+      "command": "docker",
+      "args": ["run", "-i", "--rm", "ghcr.io/apache/solr-mcp:latest"],
+      "env": {
+        "SOLR_URL": "http://localhost:8983/solr/"
+      }
+    }
+  }
+}
+ +

Using JAR:

+
{
+  "mcpServers": {
+    "solr-mcp": {
+      "command": "java",
+      "args": ["-jar", "/absolute/path/to/solr-mcp-0.0.1-SNAPSHOT.jar"],
+      "env": {
+        "SOLR_URL": "http://localhost:8983/solr/"
+      }
+    }
+  }
+}
+ +

GitHub Copilot & VSCode Extensions (Cline, Continue)

+ +

These clients use the same configuration format.

+ +

GitHub Copilot: Add to VS Code settings (settings.json)

+

Cline: Add to Cline MCP settings

+

Continue: Add to ~/.continue/config.json

+ +

Configuration (Docker):

+
{
+  "mcpServers": {
+    "solr-mcp": {
+      "command": "docker",
+      "args": ["run", "-i", "--rm", "ghcr.io/apache/solr-mcp:latest"],
+      "env": {
+        "SOLR_URL": "http://localhost:8983/solr/"
+      }
+    }
+  }
+}
+ +

Configuration (JAR):

+
{
+  "mcpServers": {
+    "solr-mcp": {
+      "command": "java",
+      "args": ["-jar", "/absolute/path/to/solr-mcp-0.0.1-SNAPSHOT.jar"],
+      "env": {
+        "SOLR_URL": "http://localhost:8983/solr/"
+      }
+    }
+  }
+}
+ +
+ Note: Continue uses a slightly different format with an array. Wrap the above configuration in: + {"mcpServers": [...]} +
+ +

JetBrains IDEs

+ +

JetBrains IDEs (IntelliJ IDEA, PyCharm, WebStorm, etc.) support MCP through AI Assistant settings.

+ +

Navigate to Settings β†’ Tools β†’ AI Assistant β†’ Model Context Protocol and add:

+
{
+  "servers": {
+    "solr-mcp": {
+      "command": "docker",
+      "args": ["run", "-i", "--rm", "ghcr.io/apache/solr-mcp:latest"],
+      "env": {
+        "SOLR_URL": "http://localhost:8983/solr/"
+      }
+    }
+  }
+}
+ +

Alternatively, edit the MCP configuration file directly:

+ + +

MCP Inspector (STDIO)

+ +

The MCP Inspector is an official tool for testing and + debugging MCP servers.

+ +

For testing with STDIO transport:

+
# Start the server
+docker run -i --rm ghcr.io/apache/solr-mcp:latest
+
+# Or using JAR
+java -jar build/libs/solr-mcp-0.0.1-SNAPSHOT.jar
+
+# In another terminal, connect with MCP Inspector
+npx @modelcontextprotocol/inspector
+ +
+ Important: After adding or modifying MCP server configurations, completely restart your client + application (quit and reopen) for changes to take effect. +
+ +

HTTP Mode

+ +

HTTP mode uses a streamable HTTP transport. This is useful for debugging with MCP Inspector or when your client + doesn't support STDIO.

+ +

Starting the Server in HTTP Mode

+ +

Using Docker:

+
docker run -d --name solr-mcp \
+  -p 8080:8080 \
+  -e PROFILES=http \
+  -e SOLR_URL=http://localhost:8983/solr/ \
+  ghcr.io/apache/solr-mcp:latest
+ +

Using JAR:

+
PROFILES=http java -jar build/libs/solr-mcp-0.0.1-SNAPSHOT.jar
+ +

Client Configuration for HTTP Mode

+ +

All clients use the same configuration format for HTTP mode:

+ +

Claude Desktop, GitHub Copilot, Cline, Continue:

+
{
+  "mcpServers": {
+    "solr-mcp": {
+      "url": "http://localhost:8080/mcp"
+    }
+  }
+}
+ +

JetBrains IDEs:

+
{
+  "servers": {
+    "solr-mcp": {
+      "url": "http://localhost:8080/mcp"
+    }
+  }
+}
+ +

MCP Inspector (HTTP):

+
npx @modelcontextprotocol/inspector http://localhost:8080/mcp
+ +
+ Tip: Use HTTP mode with MCP + Inspector for an interactive web interface to test all available tools during development. +
+ +

Environment Variables

+ + + + + + + + + + + + + + + + + + + + + +
VariableDescriptionDefault
SOLR_URLURL of the Solr instancehttp://localhost:8983/solr/
PROFILESTransport mode (empty=STDIO, http=HTTP)(empty - STDIO mode)
+ +

Docker Network Configuration

+ +
+ Connecting to Solr from Docker: +
    +
  • macOS/Windows: Use host.docker.internal instead of localhost
  • +
  • Linux: Add --network host to docker run command
  • +
  • Alternative: Link containers using Docker networks
  • +
+
+ +

Verifying Integration

+ +

Claude Desktop: Look for the πŸ”Œ icon in the bottom-right corner. Click it to see "solr-mcp" with 6 + available tools.

+ +

Other Clients: Check your client's tools/context servers panel to confirm the connection and see + available tools.

+ +

Available MCP Tools

+ +

The Solr MCP Server provides six tools accessible to AI assistants:

+ +

1. Search

+

Search Solr collections with advanced query options.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescriptionRequired
collectionStringSolr collection to queryYes
queryStringSolr query (defaults to *:*)No
filterQueriesArrayFilter queries (fq parameter)No
facetFieldsArrayFields to facet onNo
sortClausesArraySorting criteriaNo
startIntegerStarting offset for paginationNo
rowsIntegerNumber of rows to returnNo
+ +
+ Dynamic Fields: Solr uses dynamic field suffixes: +
    +
  • _s - String field (exact matching)
  • +
  • _i - Integer field
  • +
  • _l - Long field
  • +
  • _f - Float field
  • +
  • _d - Double field
  • +
  • _dt - Date field
  • +
  • _b - Boolean field
  • +
  • _t - Text field (tokenized)
  • +
+
+ +

2. index_json_documents

+

Index documents from a JSON string.

+
{
+  "collection": "myCollection",
+  "json": "[{\"id\":\"1\",\"title\":\"Example\"}]"
+}
+ +

3. index_csv_documents

+

Index documents from a CSV string.

+
{
+  "collection": "myCollection",
+  "csv": "id,title\n1,Example\n2,Another"
+}
+ +

4. index_xml_documents

+

Index documents from an XML string.

+
{
+  "collection": "myCollection",
+  "xml": "<docs><doc><field name=\"id\">1</field></doc></docs>"
+}
+ +

5. listCollections

+

List all available Solr collections. No parameters required.

+ +

6. getCollectionStats

+

Retrieve statistics and metrics for a collection.

+
{
+  "collection": "books"
+}
+ +

7. checkHealth

+

Check the health status of a collection.

+
{
+  "collection": "books"
+}
+ +

8. getSchema

+

Retrieve schema information for a collection.

+
{
+  "collection": "books"
+}
+ +

Usage Examples

+ +

Example 1: Basic Search

+

User: "Search for books about science in the books collection"

+

AI Assistant uses:

+
Search({
+  "collection": "books",
+  "query": "science"
+})
+ +

Example 2: Filtered Search with Facets

+

User: "Show me fantasy books with facets by author"

+

AI Assistant uses:

+
Search({
+  "collection": "books",
+  "query": "*:*",
+  "filterQueries": ["genre_s:fantasy"],
+  "facetFields": ["author"],
+  "rows": 10
+})
+ +

Example 3: Sorting and Pagination

+

User: "Show me the newest films, sorted by year descending, page 2"

+

AI Assistant uses:

+
Search({
+  "collection": "films",
+  "query": "*:*",
+  "sortClauses": [{"field": "initial_release_date", "order": "desc"}],
+  "start": 10,
+  "rows": 10
+})
+ +

Example 4: Indexing Documents

+

User: "Add a new book to the collection"

+

AI Assistant uses:

+
index_json_documents({
+  "collection": "books",
+  "json": "[{
+    \"id\": \"new-book-1\",
+    \"name\": [\"Introduction to Solr\"],
+    \"author\": [\"Jane Developer\"],
+    \"genre_s\": \"technical\",
+    \"price\": [29.99],
+    \"inStock\": [true]
+  }]"
+})
+ +

Example 5: Collection Analysis

+

User: "What collections are available and show me stats for books"

+

AI Assistant uses:

+
listCollections()
+
+getCollectionStats({
+  "collection": "books"
+})
+ +

Troubleshooting

+ +

Connection Issues

+ +
+ Problem: "Cannot connect to Solr"
+ Solutions: +
    +
  • Verify Solr is running: curl http://localhost:8983/solr/
  • +
  • Check SOLR_URL environment variable
  • +
  • For Docker on Linux, use --network host
  • +
  • For Docker on macOS/Windows, use host.docker.internal instead of localhost
  • +
+
+ +

MCP Client Issues

+ +
+ Problem: "Server not showing in client"
+ Solutions: +
    +
  • Verify JSON configuration syntax (use a JSON validator)
  • +
  • Use absolute paths for JAR files
  • +
  • Completely restart the client application (quit, don't just close window)
  • +
  • Check client logs for error messages
  • +
+
+ +

Docker Issues

+ +
+ Problem: "Container cannot access Solr"
+ Solutions: +
    +
  • On macOS/Windows: Set SOLR_URL=http://host.docker.internal:8983/solr/
  • +
  • On Linux: Add --network host to docker run command
  • +
  • Alternative: Create a Docker network and connect both containers
  • +
+
+ +

Common Query Patterns

+ +
+ Query Examples: +
    +
  • All documents: *:*
  • +
  • Exact match: field:"exact phrase"
  • +
  • Wildcards: field:test*
  • +
  • Range: price:[10 TO 20]
  • +
  • Boolean: field1:value1 AND field2:value2
  • +
+
+ +

Advanced Topics

+ +

Building from Source

+ +
# Clone the repository
+git clone https://github.com/apache/solr-mcp.git
+cd solr-mcp
+
+# Build with Gradle
+./gradlew build
+
+# Run tests
+./gradlew test
+
+# Build Docker image locally
+./gradlew jibDockerBuild
+ +

Custom Solr Connection

+ +

Connect to a remote Solr instance:

+
{
+  "mcpServers": {
+    "solr-mcp": {
+      "command": "docker",
+      "args": ["run", "-i", "--rm", "ghcr.io/apache/solr-mcp:latest"],
+      "env": {
+        "SOLR_URL": "https://remote-solr.example.com:8983/solr/"
+      }
+    }
+  }
+}
+ +
+ Note: The current version supports basic Solr connections. Authentication support is planned for + future releases. +
+ +

Performance Optimization

+ + + +

Security Best Practices

+ + + +

Additional Resources

+ + + +
+ +

This guide was created for Solr MCP Server version 0.0.1-SNAPSHOT. Last updated: November 2025.

+ +

License: Apache License 2.0

+ + \ No newline at end of file diff --git a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java index c121914..21639fa 100644 --- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java +++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java @@ -21,7 +21,7 @@ import static org.apache.solr.mcp.server.collection.CollectionUtils.getLong; import static org.apache.solr.mcp.server.util.JsonUtils.toJson; -import com.fasterxml.jackson.databind.ObjectMapper; + import io.micrometer.observation.annotation.Observed; import java.io.IOException; import java.util.ArrayList; @@ -51,6 +51,7 @@ import org.springaicommunity.mcp.annotation.McpToolParam; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; +import tools.jackson.databind.ObjectMapper; /** * Spring Service providing comprehensive Solr collection management and diff --git a/src/main/java/org/apache/solr/mcp/server/config/InstallOpenTelemetryAppender.java b/src/main/java/org/apache/solr/mcp/server/config/InstallOpenTelemetryAppender.java new file mode 100644 index 0000000..27d11b6 --- /dev/null +++ b/src/main/java/org/apache/solr/mcp/server/config/InstallOpenTelemetryAppender.java @@ -0,0 +1,21 @@ +package org.apache.solr.mcp.server.config; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +@Component +class InstallOpenTelemetryAppender implements InitializingBean { + + private final OpenTelemetry openTelemetry; + + InstallOpenTelemetryAppender(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + @Override + public void afterPropertiesSet() { + OpenTelemetryAppender.install(this.openTelemetry); + } +} diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java index 7359ccf..ca3422d 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java +++ b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java @@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.impl.Http2SolrClient; +import org.apache.solr.client.solrj.impl.HttpJdkSolrClient; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -93,10 +93,10 @@ * * * @version 1.0.0 - * @since 1.0.0 * @see SolrConfigurationProperties - * @see Http2SolrClient + * @see HttpJdkSolrClient * @see org.springframework.boot.context.properties.EnableConfigurationProperties + * @since 1.0.0 */ @Configuration @EnableConfigurationProperties(SolrConfigurationProperties.class) @@ -138,10 +138,11 @@ public class SolrConfig { * Client Type: * *

- * Creates an {@code HttpSolrClient} configured for standard HTTP-based - * communication with Solr servers. This client type is suitable for both - * standalone Solr instances and SolrCloud deployments when used with load - * balancers. + * Creates an {@code HttpJdkSolrClient} configured for standard HTTP-based + * communication with Solr servers using the JDK's built-in HTTP client. This + * avoids Jetty version conflicts between SolrJ and Spring Boot. This client + * type is suitable for both standalone Solr instances and SolrCloud deployments + * when used with load balancers. * *

* Error Handling: @@ -156,7 +157,7 @@ public class SolrConfig { * *

* @@ -164,7 +165,7 @@ public class SolrConfig { * the injected Solr configuration properties containing connection * URL * @return configured SolrClient instance ready for use in application services - * @see Http2SolrClient.Builder + * @see HttpJdkSolrClient.Builder * @see SolrConfigurationProperties#url() */ @Bean @@ -186,8 +187,9 @@ SolrClient solrClient(SolrConfigurationProperties properties) { } } - // Use with explicit base URL - return new Http2SolrClient.Builder(url).withConnectionTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) + // Use HttpJdkSolrClient which uses the JDK's built-in HTTP client + // This avoids Jetty version conflicts between SolrJ and Spring Boot + return new HttpJdkSolrClient.Builder(url).withConnectionTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) .withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS).build(); } } diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java b/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java index bc59667..c69700f 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java +++ b/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java @@ -110,10 +110,10 @@ * @param url * the base URL of the Apache Solr server (required, non-null) * @version 1.0.0 - * @since 1.0.0 * @see SolrConfig * @see org.springframework.boot.context.properties.ConfigurationProperties * @see org.springframework.boot.context.properties.EnableConfigurationProperties + * @since 1.0.0 */ @ConfigurationProperties(prefix = "solr") public record SolrConfigurationProperties(String url) { diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java index 1a8a149..a387d92 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java @@ -16,9 +16,6 @@ */ package org.apache.solr.mcp.server.indexing.documentcreator; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -26,6 +23,9 @@ import java.util.Set; import org.apache.solr.common.SolrInputDocument; import org.springframework.stereotype.Component; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; /** * Utility class for processing JSON documents and converting them to @@ -111,7 +111,7 @@ public List create(String json) throws DocumentProcessingExce List documents = new ArrayList<>(); try { - ObjectMapper mapper = new ObjectMapper(); + JsonMapper mapper = JsonMapper.builder().build(); JsonNode rootNode = mapper.readTree(json); if (rootNode.isArray()) { @@ -123,7 +123,7 @@ public List create(String json) throws DocumentProcessingExce documents.add(doc); } } - } catch (IOException e) { + } catch (JacksonException e) { throw new DocumentProcessingException("Failed to parse JSON document", e); } @@ -250,6 +250,6 @@ private Object convertJsonValue(JsonNode value) { return value.asDouble(); if (value.isInt()) return value.asInt(); - return value.asText(); + return value.asString(); } } diff --git a/src/main/java/org/apache/solr/mcp/server/metadata/SchemaService.java b/src/main/java/org/apache/solr/mcp/server/metadata/SchemaService.java index c55b35b..a961f0f 100644 --- a/src/main/java/org/apache/solr/mcp/server/metadata/SchemaService.java +++ b/src/main/java/org/apache/solr/mcp/server/metadata/SchemaService.java @@ -18,7 +18,7 @@ import static org.apache.solr.mcp.server.util.JsonUtils.toJson; -import com.fasterxml.jackson.databind.ObjectMapper; + import io.micrometer.observation.annotation.Observed; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.request.schema.SchemaRequest; @@ -26,6 +26,7 @@ import org.springaicommunity.mcp.annotation.McpResource; import org.springaicommunity.mcp.annotation.McpTool; import org.springframework.stereotype.Service; +import tools.jackson.databind.ObjectMapper; /** * Spring Service providing schema introspection and management capabilities for diff --git a/src/main/java/org/apache/solr/mcp/server/util/JsonUtils.java b/src/main/java/org/apache/solr/mcp/server/util/JsonUtils.java index 6ecc3bc..23cf66c 100644 --- a/src/main/java/org/apache/solr/mcp/server/util/JsonUtils.java +++ b/src/main/java/org/apache/solr/mcp/server/util/JsonUtils.java @@ -16,8 +16,8 @@ */ package org.apache.solr.mcp.server.util; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; /** * Utility class for JSON serialization operations. @@ -51,7 +51,7 @@ private JsonUtils() { public static String toJson(ObjectMapper objectMapper, Object obj) { try { return objectMapper.writeValueAsString(obj); - } catch (JsonProcessingException e) { + } catch (JacksonException _) { return "{\"error\": \"Failed to serialize response\"}"; } } diff --git a/src/main/resources/application-http.properties b/src/main/resources/application-http.properties index 6d79619..2ffc94e 100644 --- a/src/main/resources/application-http.properties +++ b/src/main/resources/application-http.properties @@ -2,6 +2,7 @@ spring.main.web-application-type=servlet spring.ai.mcp.server.type=sync spring.ai.mcp.server.protocol=stateless spring.ai.mcp.server.stdio=false + # OAuth2 Security Configuration # Configure the issuer URI for your OAuth2 authorization server # For Auth0: https:///.well-known/openid-configuration @@ -10,12 +11,42 @@ spring.ai.mcp.server.stdio=false spring.security.oauth2.resourceserver.jwt.issuer-uri=${OAUTH2_ISSUER_URI:https://your-auth0-domain.auth0.com/} # Security toggle - set to true to enable OAuth2 authentication, false to bypass spring.security.enabled=${SECURITY_ENABLED:false} -# observability + +# ============================================================================= +# OpenTelemetry Configuration (HTTP mode only) +# ============================================================================= +# Provides distributed tracing, metrics, and log export via OTLP protocol. +# See Observability.md for detailed setup instructions. +# +# LOCAL DEVELOPMENT: +# Run `docker compose up -d lgtm` - Spring Boot Docker Compose will +# auto-detect the grafana/otel-lgtm container and configure OTLP endpoints. +# +# PRODUCTION: +# Set environment variables to override endpoints: +# - OTEL_METRICS_URL=https://collector.example.com/v1/metrics +# - OTEL_TRACES_URL=https://collector.example.com/v1/traces +# - OTEL_LOGS_URL=https://collector.example.com/v1/logs + +# Application name for telemetry identification +spring.application.name=solr-mcp-server + management.endpoints.web.exposure.include=health,sbom,metrics,info,loggers,prometheus -# Enable @Observed annotation support for custom spans management.observations.annotations.enabled=true + # Tracing Configuration # Set to 1.0 for 100% sampling in development, lower in production (e.g., 0.1) management.tracing.sampling.probability=${OTEL_SAMPLING_PROBABILITY:1.0} -otel.exporter.otlp.endpoint=${OTEL_TRACES_URL:http://localhost:4317} -otel.exporter.otlp.protocol=grpc +# OTLP endpoints - auto-configured by Spring Boot Docker Compose when lgtm is running +# Spring Boot will detect the LGTM container and automatically configure these URLs +# Override with environment variables for production deployments (e.g., OTEL_TRACES_URL) +# OTLP Metrics Export +# Endpoint for metrics export (Prometheus-compatible via OTLP) +management.otlp.metrics.export.url=${OTEL_METRICS_URL:http://localhost:4318/v1/metrics} +# OTLP Tracing Export +# Endpoint for distributed trace export +management.opentelemetry.tracing.export.otlp.endpoint=${OTEL_TRACES_URL:http://localhost:4318/v1/traces} +# OTLP Logging Export +# Endpoint for log export (requires logback-spring.xml configuration) +management.opentelemetry.logging.export.otlp.endpoint=${OTEL_LOGS_URL:http://localhost:4318/v1/logs} + diff --git a/src/test/java/org/apache/solr/mcp/server/MainTest.java b/src/test/java/org/apache/solr/mcp/server/MainTest.java index 1b9ae42..f3a23c4 100644 --- a/src/test/java/org/apache/solr/mcp/server/MainTest.java +++ b/src/test/java/org/apache/solr/mcp/server/MainTest.java @@ -22,7 +22,6 @@ import org.apache.solr.mcp.server.search.SearchService; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; /** @@ -32,7 +31,6 @@ * dependencies. */ @SpringBootTest -@ActiveProfiles("test") class MainTest { @MockitoBean diff --git a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java index ed5bca8..4f14b29 100644 --- a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java +++ b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java @@ -21,7 +21,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.lang.reflect.Method; import java.util.Arrays; @@ -41,6 +40,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import tools.jackson.databind.ObjectMapper; @ExtendWith(MockitoExtension.class) class CollectionServiceTest { diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java index 342ddd0..c0bae61 100644 --- a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java @@ -19,7 +19,7 @@ import static org.junit.jupiter.api.Assertions.*; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.impl.Http2SolrClient; +import org.apache.solr.client.solrj.impl.HttpJdkSolrClient; import org.apache.solr.mcp.server.TestcontainersConfiguration; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -51,9 +51,8 @@ void testSolrClientConfiguration() { // Verify that the SolrClient is using the correct URL // Note: SolrConfig normalizes the URL to have trailing slash, but - // Http2SolrClient removes - // it - var httpSolrClient = assertInstanceOf(Http2SolrClient.class, solrClient); + // HttpJdkSolrClient removes it + var httpSolrClient = assertInstanceOf(HttpJdkSolrClient.class, solrClient); String expectedUrl = "http://" + solrContainer.getHost() + ":" + solrContainer.getMappedPort(8983) + "/solr"; assertEquals(expectedUrl, httpSolrClient.getBaseURL()); } @@ -84,13 +83,13 @@ void testUrlNormalization(String inputUrl, String expectedUrl) { SolrClient client = solrConfig.solrClient(testProperties); assertNotNull(client); - var httpClient = assertInstanceOf(Http2SolrClient.class, client); + var httpClient = assertInstanceOf(HttpJdkSolrClient.class, client); assertEquals(expectedUrl, httpClient.getBaseURL()); // Clean up try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } } @@ -102,14 +101,14 @@ void testUrlWithoutTrailingSlash() { SolrConfig solrConfig = new SolrConfig(); SolrClient client = solrConfig.solrClient(testProperties); - Http2SolrClient httpClient = (Http2SolrClient) client; + HttpJdkSolrClient httpClient = (HttpJdkSolrClient) client; // Should add trailing slash and solr path assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } } @@ -121,14 +120,14 @@ void testUrlWithTrailingSlashButNoSolrPath() { SolrConfig solrConfig = new SolrConfig(); SolrClient client = solrConfig.solrClient(testProperties); - Http2SolrClient httpClient = (Http2SolrClient) client; + HttpJdkSolrClient httpClient = (HttpJdkSolrClient) client; // Should add solr path to existing trailing slash assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } } @@ -140,14 +139,14 @@ void testUrlWithSolrPathButNoTrailingSlash() { SolrConfig solrConfig = new SolrConfig(); SolrClient client = solrConfig.solrClient(testProperties); - Http2SolrClient httpClient = (Http2SolrClient) client; + HttpJdkSolrClient httpClient = (HttpJdkSolrClient) client; // Should add trailing slash assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } } @@ -159,14 +158,14 @@ void testUrlAlreadyProperlyFormatted() { SolrConfig solrConfig = new SolrConfig(); SolrClient client = solrConfig.solrClient(testProperties); - Http2SolrClient httpClient = (Http2SolrClient) client; + HttpJdkSolrClient httpClient = (HttpJdkSolrClient) client; // Should remain unchanged assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } } diff --git a/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceTest.java b/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceTest.java index 964c3a5..bb70cb2 100644 --- a/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceTest.java +++ b/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceTest.java @@ -21,7 +21,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; -import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; @@ -33,6 +32,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import tools.jackson.databind.ObjectMapper; /** * Comprehensive test suite for the SchemaService class. Tests schema retrieval diff --git a/src/test/java/org/apache/solr/mcp/server/observability/InMemoryTracingTestConfiguration.java b/src/test/java/org/apache/solr/mcp/server/observability/InMemoryTracingTestConfiguration.java new file mode 100644 index 0000000..56af46f --- /dev/null +++ b/src/test/java/org/apache/solr/mcp/server/observability/InMemoryTracingTestConfiguration.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.mcp.server.observability; + +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * Minimal test configuration that provides InMemorySpanExporter bean. + *

+ * Spring Boot's opentelemetry-test starter requires this to be explicitly + * configured. + */ +@TestConfiguration +public class InMemoryTracingTestConfiguration { + + @Bean + public InMemorySpanExporter inMemorySpanExporter() { + return InMemorySpanExporter.create(); + } + +}