diff --git a/README.md b/README.md index 565905071..e9d749357 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ of them assume you have docker running on your local machine. ## Example modules: +- [OpenTelemetry Reference Application](reference-application) + - A comprehensive reference application demonstrating OpenTelemetry usage following the + [Getting Started Reference Application Specification](https://opentelemetry.io/docs/getting-started/reference-application-specification/). + - Includes traces, metrics, logs, manual instrumentation, Docker setup with collector, and multiple configuration approaches. - [Using the SDK AutoConfiguration module](autoconfigure) - This module contains a fully-functional example of using the autoconfigure SDK extension module to configure the SDK using only environment diff --git a/reference-application/Dockerfile b/reference-application/Dockerfile new file mode 100644 index 000000000..fb9d4477c --- /dev/null +++ b/reference-application/Dockerfile @@ -0,0 +1,13 @@ +FROM openjdk:17-jdk-slim + +WORKDIR /app + +# Copy the JAR file and agent +COPY build/libs/app.jar /app.jar +COPY build/agent/opentelemetry-javaagent.jar /opentelemetry-javaagent.jar + +# Expose the port +EXPOSE 8080 + +# Run the application with the Java agent +ENTRYPOINT ["java", "-javaagent:/opentelemetry-javaagent.jar", "-jar", "/app.jar"] \ No newline at end of file diff --git a/reference-application/E2E-TEST.md b/reference-application/E2E-TEST.md new file mode 100644 index 000000000..e2c957e99 --- /dev/null +++ b/reference-application/E2E-TEST.md @@ -0,0 +1,88 @@ +# End-to-End Test + +This directory contains an end-to-end test (`test-e2e.sh`) that validates the complete OpenTelemetry reference application stack. + +## What it tests + +The test verifies: +1. **Application functionality**: All endpoints return correct responses +2. **OpenTelemetry integration**: Telemetry data is generated and exported +3. **Collector functionality**: OTLP data is received and processed +4. **Prometheus integration**: Metrics are scraped and available +5. **Docker Compose setup**: All services start and work together + +## Running the test + +### Prerequisites + +- Docker +- Docker Compose (or `docker compose`) +- curl +- jq + +### Execution + +```bash +# Via Gradle +../gradlew e2eTest + +# Directly +./test-e2e.sh + +# Dry-run (validation only) +./test-e2e.sh --dry-run +``` + +## Test flow + +1. **Setup**: Cleans any existing containers and builds fresh images +2. **Start services**: Runs `docker-compose up --build -d` +3. **Wait for readiness**: Polls health endpoints until services are ready +4. **Functional tests**: Tests all application endpoints +5. **Telemetry validation**: Verifies OpenTelemetry data collection +6. **Integration tests**: Checks collector and Prometheus functionality +7. **Cleanup**: Stops and removes all containers and volumes + +## What's tested + +### Application Endpoints +- `GET /rolldice` - Basic dice rolling +- `GET /rolldice?player=testuser` - Parameterized requests +- `GET /rolldice?rolls=3` - Multiple dice rolls +- `GET /health` - Health check + +### OpenTelemetry Integration +- Java Agent instrumentation +- Custom span creation +- Metrics generation +- Log correlation +- OTLP export to collector + +### Telemetry Testing +In addition to end-to-end infrastructure testing, the application includes **telemetry testing** that validates actual OpenTelemetry data export: + +- **Trace validation**: Verifies spans are created with correct names, attributes, and events +- **Metric validation**: Confirms custom metrics are exported properly +- **Baggage testing**: Validates cross-cutting concern propagation +- **MockServer integration**: Captures OTLP requests for detailed analysis + +These tests run with the OpenTelemetry Java Agent and use the same protobuf parsing as the telemetry-testing example. + +### Infrastructure +- OpenTelemetry Collector OTLP ingestion +- Prometheus metrics scraping +- Service networking and dependencies + +## Using in CI/CD + +This test can be integrated into CI/CD pipelines to ensure the reference application works correctly in a production-like environment. + +Example GitHub Actions usage: +```yaml +- name: Run end-to-end test + run: | + cd reference-application + ./test-e2e.sh +``` + +The test automatically handles cleanup and provides clear success/failure indicators. \ No newline at end of file diff --git a/reference-application/README.md b/reference-application/README.md new file mode 100644 index 000000000..b74aa78d4 --- /dev/null +++ b/reference-application/README.md @@ -0,0 +1,217 @@ +# OpenTelemetry Java Reference Application + +This reference application demonstrates comprehensive OpenTelemetry usage in Java, following the [OpenTelemetry Getting Started Reference Application Specification](https://opentelemetry.io/docs/getting-started/reference-application-specification/). + +## Features + +This application showcases: + +- **Traces**: Manual and automatic span creation, distributed tracing +- **Metrics**: Custom metrics, performance monitoring +- **Logs**: Structured logging with trace correlation +- **Multiple exporters**: Console, OTLP, file-based exports +- **Configuration**: Environment variables, programmatic setup, and declarative configuration +- **Docker support**: Complete setup with OpenTelemetry Collector + +## Application Overview + +The reference application is a dice rolling service that demonstrates OpenTelemetry capabilities using the **OpenTelemetry Java Agent** for automatic instrumentation and manual instrumentation examples: + +### Endpoints + +- `GET /rolldice` - Basic dice roll (returns random 1-6) +- `GET /rolldice?player=` - Dice roll for a specific player +- `GET /rolldice?rolls=` - Roll multiple dice +- `GET /health` - Health check endpoint + +### Scenarios Demonstrated + +1. **Basic HTTP instrumentation**: Automatic span creation for HTTP requests +2. **Manual instrumentation**: Custom spans for business logic +3. **Error handling**: Error span recording and exception tracking +4. **Custom metrics**: Performance counters, histograms, gauges +5. **Baggage propagation**: Cross-cutting concerns +6. **Resource detection**: Automatic resource attribute detection + +## Quick Start + +### Prerequisites + +- Java 17 or later +- Docker and Docker Compose (for collector setup) + +### Running with Console Output + +```shell +# Build the application with the Java agent +../gradlew bootJar + +# Run with the Java agent for automatic instrumentation +java -javaagent:build/agent/opentelemetry-javaagent.jar -jar build/libs/app.jar +``` + +Then test the endpoints: +```shell +curl http://localhost:8080/rolldice +curl http://localhost:8080/rolldice?player=alice +curl http://localhost:8080/rolldice?rolls=3 +``` + +### Running with OpenTelemetry Collector + +```shell +# Build the application +../gradlew bootJar + +# Start the collector and application +docker-compose up --build +``` + +This will: +- Start the reference application on port 8080 +- Start OpenTelemetry Collector on port 4317/4318 +- Export telemetry data to the collector +- Output structured telemetry data to console + +## Configuration + +The application supports multiple configuration approaches: + +### Environment Variables + +```shell +export OTEL_SERVICE_NAME=dice-server +export OTEL_SERVICE_VERSION=1.0.0 +export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +export OTEL_TRACES_EXPORTER=otlp +export OTEL_METRICS_EXPORTER=otlp +export OTEL_LOGS_EXPORTER=otlp +``` + +### Java Agent Configuration + +The application uses the OpenTelemetry Java Agent which automatically configures instrumentation based on environment variables and system properties. All standard OpenTelemetry configuration options are supported. + +### Declarative Configuration + +Use the included `otel-config.yaml` for file-based configuration: + +```shell +export OTEL_EXPERIMENTAL_CONFIG_FILE=otel-config.yaml +``` + +## Understanding the Output + +### Traces + +The application creates spans for: +- HTTP requests (automatic) +- Business logic operations (manual) +- External calls and computations +- Error scenarios + +### Metrics + +The application reports: +- Request duration histograms +- Request counters by endpoint +- Error rates +- Custom business metrics (dice roll distributions) + +### Logs + +All logs include: +- Trace ID and Span ID for correlation +- Structured fields +- Different log levels +- Business context + +## Development + +### Building + +```shell +../gradlew build +``` + +### Testing + +The reference application includes comprehensive testing: + +#### Unit Tests +```shell +../gradlew test +``` + +The test suite includes: +- **Functional tests**: Verify all endpoints return correct responses +- **Telemetry tests**: Validate OpenTelemetry data export using MockServer + - Traces: HTTP spans, custom spans, span attributes, and events + - Metrics: Custom counters and timers + - Baggage: Cross-cutting concern propagation + +The telemetry tests use MockServer to capture OTLP requests and verify that the application correctly generates and exports telemetry data for different scenarios. + +For detailed information about telemetry testing, see [TELEMETRY-TESTING.md](TELEMETRY-TESTING.md). + +### End-to-End Testing + +Run the comprehensive end-to-end test that verifies the complete OpenTelemetry stack: + +```shell +# Run via Gradle +../gradlew e2eTest + +# Or run directly +./test-e2e.sh +``` + +This test: +- Builds and starts all services using `docker-compose up --build` +- Waits for services to be ready (application, collector, Prometheus) +- Tests all application endpoints +- Verifies OpenTelemetry data collection and export +- Validates Prometheus metric scraping +- Cleans up resources automatically + +For detailed information about the end-to-end test, see [E2E-TEST.md](E2E-TEST.md). + +### Running locally + +```shell +../gradlew bootRun +``` + +## Docker Images + +The application can be built as a Docker image: + +```shell +../gradlew bootBuildImage +``` + +## Troubleshooting + +### Common Issues + +1. **No telemetry data**: Check OTEL_* environment variables +2. **Connection issues**: Verify collector endpoint configuration +3. **Missing traces**: Ensure sampling is configured correctly + +### Debugging + +Enable debug logging: +```shell +export OTEL_JAVAAGENT_DEBUG=true +``` + +Or set logging level: +```shell +export LOGGING_LEVEL_IO_OPENTELEMETRY=DEBUG +``` + +## Learn More + +- [OpenTelemetry Java Documentation](https://opentelemetry.io/docs/languages/java/) +- [OpenTelemetry Specification](https://opentelemetry.io/docs/specs/otel/) +- [Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/) \ No newline at end of file diff --git a/reference-application/TELEMETRY-TESTING.md b/reference-application/TELEMETRY-TESTING.md new file mode 100644 index 000000000..ad6740e63 --- /dev/null +++ b/reference-application/TELEMETRY-TESTING.md @@ -0,0 +1,143 @@ +# Telemetry Testing + +This document explains the telemetry testing approach used in the reference application to validate OpenTelemetry data export. + +## Overview + +The `TelemetryTest` class demonstrates how to test that your application correctly generates and exports OpenTelemetry telemetry data. This approach is based on the [telemetry-testing example](../telemetry-testing) in the repository. + +## How it Works + +### MockServer Setup +- Uses [MockServer](https://www.mock-server.com/) to simulate an OTLP collector +- Listens on port 4318 (default OTLP HTTP endpoint) +- Captures all OTLP requests sent by the OpenTelemetry Java Agent + +### Test Configuration +The test suite is configured to: +- Run with OpenTelemetry Java Agent attached (`-javaagent`) +- Export telemetry using `http/protobuf` protocol +- Set faster metric export interval (5 seconds vs default 60 seconds) +- Suppress MockServer logging to avoid circular telemetry + +### Protobuf Parsing +Uses OpenTelemetry protocol buffers to parse captured requests: +- `ExportTraceServiceRequest` for traces/spans +- `ExportMetricsServiceRequest` for metrics +- Direct access to span attributes, events, and metric values + +## What Gets Tested + +### Traces +- **HTTP spans**: Automatic instrumentation spans (e.g., `GET /rolldice`) +- **Custom spans**: Manual spans created in application code (e.g., `roll-dice`) +- **Span hierarchy**: Parent-child relationships between spans +- **Attributes**: Custom attributes like `dice.player`, `dice.rolls` +- **Events**: Custom events like `dice-rolled` +- **Error handling**: Exception recording and error status + +### Metrics +- **Custom counters**: `dice_rolls_total` +- **OpenTelemetry metrics**: Metrics created via OpenTelemetry API and exported by the Java Agent + +### Baggage +- **Cross-cutting data**: Player names, request types +- **Propagation**: Baggage values accessible across span boundaries + +## Test Scenarios + +### Basic Functionality +```java +@Test +public void testDiceRollTelemetry() { + // Call endpoint + template.getForEntity("/rolldice", String.class); + + // Verify spans are created + await().untilAsserted(() -> { + var spans = extractSpansFromRequests(requests); + assertThat(spans) + .extracting(Span::getName) + .contains("GET /rolldice", "roll-dice"); + }); +} +``` + +### Complex Scenarios +- **Multiple operations**: Testing endpoints that create multiple spans +- **Parameterized requests**: Verifying span attributes contain request parameters +- **Error conditions**: Testing that exceptions are properly recorded +- **Performance scenarios**: Large computations that generate multiple events + +## Implementation Details + +### Java Agent Configuration +```kotlin +jvmArgs = listOf( + "-javaagent:${agentJarPath}", + "-Dotel.metric.export.interval=5000", + "-Dotel.exporter.otlp.protocol=http/protobuf", + "-Dmockserver.logLevel=off" +) +``` + +### Request Parsing +```java +private List extractSpansFromRequests(HttpRequest[] requests) { + return Arrays.stream(requests) + .map(HttpRequest::getBody) + .flatMap(body -> getExportTraceServiceRequest(body).stream()) + .flatMap(r -> r.getResourceSpansList().stream()) + .flatMap(r -> r.getScopeSpansList().stream()) + .flatMap(r -> r.getSpansList().stream()) + .collect(Collectors.toList()); +} +``` + +## Benefits + +### Comprehensive Validation +- **End-to-end verification**: Tests actual OTLP export, not just in-memory data +- **Protocol-level testing**: Uses the same protobuf format as real collectors +- **Realistic conditions**: Tests with the actual Java Agent configuration + +### Development Confidence +- **Regression testing**: Catch telemetry regressions before production +- **Documentation**: Tests serve as living examples of expected telemetry +- **Debugging**: Easy to inspect actual telemetry data during development + +## Running the Tests + +```bash +# Run all tests (including telemetry tests) +../gradlew test + +# Run only telemetry tests +../gradlew test --tests "TelemetryTest" + +# Run specific test method +../gradlew test --tests "TelemetryTest.testDiceRollTelemetry" +``` + +## Troubleshooting + +### Common Issues + +1. **Port conflicts**: Ensure port 4318 is available +2. **Timing issues**: Use appropriate `await()` timeouts for telemetry export +3. **Agent not loaded**: Verify Java Agent is properly attached in test configuration +4. **Network issues**: MockServer requires localhost connectivity + +### Debugging Tips + +- Enable debug logging: `-Dotel.javaagent.debug=true` +- Inspect raw requests: Log `request.getBodyAsString()` before parsing +- Check span timing: Some spans may export in separate batches +- Verify test isolation: Each test should reset MockServer expectations + +## Further Reading + +- [OpenTelemetry Java Agent Configuration](https://opentelemetry.io/docs/languages/java/configuration/) +- [MockServer Documentation](https://www.mock-server.com/) +- [OpenTelemetry Protocol Specification](https://opentelemetry.io/docs/specs/otlp/) +- [Telemetry Testing Example](../telemetry-testing/README.md) \ No newline at end of file diff --git a/reference-application/build.gradle.kts b/reference-application/build.gradle.kts new file mode 100644 index 000000000..f13b4ac3a --- /dev/null +++ b/reference-application/build.gradle.kts @@ -0,0 +1,59 @@ +import org.springframework.boot.gradle.plugin.SpringBootPlugin +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + id("java") + id("org.springframework.boot") version "3.5.6" +} + +val moduleName by extra { "io.opentelemetry.examples.reference-application" } + +repositories { + mavenCentral() +} + +val agent = configurations.create("agent") + +dependencies { + implementation(platform(SpringBootPlugin.BOM_COORDINATES)) + implementation("io.opentelemetry:opentelemetry-api") + + // Spring Boot + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-actuator") + + // Java agent + agent("io.opentelemetry.javaagent:opentelemetry-javaagent:2.20.1") + + // Testing + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") +} + +val copyAgent = tasks.register("copyAgent") { + from(agent.singleFile) + into(layout.buildDirectory.dir("agent")) + rename("opentelemetry-javaagent-.*\\.jar", "opentelemetry-javaagent.jar") +} + +tasks.named("bootJar") { + dependsOn(copyAgent) + archiveFileName = "app.jar" +} + +tasks.withType { + useJUnitPlatform() +} + +tasks.register("e2eTest") { + group = "verification" + description = "Run end-to-end test using docker-compose" + commandLine("./test-e2e.sh") + dependsOn("bootJar") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} \ No newline at end of file diff --git a/reference-application/docker-compose.yml b/reference-application/docker-compose.yml new file mode 100644 index 000000000..0585c450d --- /dev/null +++ b/reference-application/docker-compose.yml @@ -0,0 +1,37 @@ +services: + dice-server: + build: . + ports: + - "8080:8080" + environment: + - OTEL_SERVICE_NAME=dice-server + - OTEL_SERVICE_VERSION=1.0.0 + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 + - OTEL_TRACES_EXPORTER=otlp + - OTEL_METRICS_EXPORTER=otlp + - OTEL_LOGS_EXPORTER=otlp + - OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf + depends_on: + - otel-collector + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + ports: + - "4317:4317" # OTLP gRPC + - "4318:4318" # OTLP HTTP + - "8889:8889" # Prometheus metrics + volumes: + - ./otel-collector-config.yaml:/etc/otelcol-contrib/otel-collector-config.yaml + command: ["--config=/etc/otelcol-contrib/otel-collector-config.yaml"] + + prometheus: + image: prom/prometheus:latest + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' \ No newline at end of file diff --git a/reference-application/otel-collector-config.yaml b/reference-application/otel-collector-config.yaml new file mode 100644 index 000000000..02501cbc8 --- /dev/null +++ b/reference-application/otel-collector-config.yaml @@ -0,0 +1,47 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + memory_limiter: + limit_mib: 512 + +exporters: + logging: + loglevel: info + sampling_initial: 5 + sampling_thereafter: 200 + + prometheus: + endpoint: "0.0.0.0:8889" + metric_expiration: 180m + enable_open_metrics: true + +extensions: + health_check: + endpoint: 0.0.0.0:13133 + pprof: + endpoint: 0.0.0.0:1777 + zpages: + endpoint: 0.0.0.0:55679 + +service: + extensions: [health_check, pprof, zpages] + pipelines: + traces: + receivers: [otlp] + processors: [memory_limiter, batch] + exporters: [logging] + metrics: + receivers: [otlp] + processors: [memory_limiter, batch] + exporters: [logging, prometheus] + logs: + receivers: [otlp] + processors: [memory_limiter, batch] + exporters: [logging] \ No newline at end of file diff --git a/reference-application/otel-config.yaml b/reference-application/otel-config.yaml new file mode 100644 index 000000000..8b0e08ab1 --- /dev/null +++ b/reference-application/otel-config.yaml @@ -0,0 +1,39 @@ +file_format: 0.3 + +# Service configuration +resource: + attributes: + service.name: dice-server + service.version: 1.0.0 + deployment.environment: local + +# Tracer provider configuration +tracer_provider: + processors: + - batch: + exporter: + console: {} + sampler: + parent_based: + root: + always_on: {} + +# Meter provider configuration +meter_provider: + readers: + - periodic: + exporter: + console: {} + interval: 30000 + +# Logger provider configuration +logger_provider: + processors: + - batch: + exporter: + console: {} + +# Propagators +propagators: + - tracecontext + - baggage \ No newline at end of file diff --git a/reference-application/prometheus.yml b/reference-application/prometheus.yml new file mode 100644 index 000000000..49eb9afc4 --- /dev/null +++ b/reference-application/prometheus.yml @@ -0,0 +1,12 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'otel-collector' + static_configs: + - targets: ['otel-collector:8889'] + + - job_name: 'dice-server' + static_configs: + - targets: ['dice-server:8080'] + metrics_path: '/actuator/prometheus' \ No newline at end of file diff --git a/reference-application/src/main/java/io/opentelemetry/example/ReferenceApplication.java b/reference-application/src/main/java/io/opentelemetry/example/ReferenceApplication.java new file mode 100644 index 000000000..7f5942470 --- /dev/null +++ b/reference-application/src/main/java/io/opentelemetry/example/ReferenceApplication.java @@ -0,0 +1,20 @@ +package io.opentelemetry.example; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class ReferenceApplication { + + public static void main(String[] args) { + SpringApplication.run(ReferenceApplication.class, args); + } + + @Bean + public OpenTelemetry openTelemetry() { + return GlobalOpenTelemetry.get(); + } +} diff --git a/reference-application/src/main/java/io/opentelemetry/example/RollController.java b/reference-application/src/main/java/io/opentelemetry/example/RollController.java new file mode 100644 index 000000000..9f5aabe67 --- /dev/null +++ b/reference-application/src/main/java/io/opentelemetry/example/RollController.java @@ -0,0 +1,170 @@ +package io.opentelemetry.example; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.baggage.BaggageBuilder; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class RollController { + private static final Logger logger = LoggerFactory.getLogger(RollController.class); + + @Autowired private OpenTelemetry openTelemetry; + + private final Tracer tracer; + private final LongCounter diceRollCounter; + + public RollController(@Autowired OpenTelemetry openTelemetry) { + this.tracer = openTelemetry.getTracer("dice-server", "1.0.0"); + Meter meter = openTelemetry.getMeter("dice-server"); + this.diceRollCounter = + meter + .counterBuilder("dice_rolls_total") + .setDescription("Total number of dice rolls") + .build(); + } + + @GetMapping("/rolldice") + public Map rollDice( + @RequestParam("player") Optional player, + @RequestParam("rolls") Optional rolls) { + + long startTime = System.nanoTime(); + + Span span = + tracer + .spanBuilder("roll-dice") + .setSpanKind(SpanKind.SERVER) + .setAttribute("dice.player", player.orElse("anonymous")) + .setAttribute("dice.rolls", rolls.orElse(1)) + .startSpan(); + + try (Scope scope = span.makeCurrent()) { + // Add baggage for cross-cutting concerns + BaggageBuilder baggageBuilder = Baggage.current().toBuilder(); + if (player.isPresent()) { + baggageBuilder.put("player.name", player.get()); + } + baggageBuilder.put("request.type", "dice-roll"); + + try (Scope baggageScope = baggageBuilder.build().makeCurrent()) { + int numRolls = rolls.orElse(1); + if (numRolls < 1 || numRolls > 10) { + throw new IllegalArgumentException("Number of rolls must be between 1 and 10"); + } + + int[] results = new int[numRolls]; + for (int i = 0; i < numRolls; i++) { + results[i] = rollSingleDie(); + } + + // Record metrics using OpenTelemetry + diceRollCounter.add(1); + long duration = System.nanoTime() - startTime; + + String playerName = player.orElse("Anonymous player"); + if (numRolls == 1) { + logger.info("{} is rolling the dice: {}", playerName, results[0]); + } else { + logger.info( + "{} is rolling {} dice: {}", + playerName, + numRolls, + java.util.Arrays.toString(results)); + } + + span.addEvent( + "dice-rolled", + Attributes.builder() + .put("dice.result", java.util.Arrays.toString(results)) + .put("dice.sum", java.util.Arrays.stream(results).sum()) + .put("dice.duration_ms", duration / 1_000_000) + .build()); + + Map response = new HashMap<>(); + if (numRolls == 1) { + response.put("result", results[0]); + } else { + response.put("results", results); + response.put("sum", java.util.Arrays.stream(results).sum()); + } + response.put("player", playerName); + + return response; + } + } catch (Exception e) { + span.recordException(e); + span.setStatus(StatusCode.ERROR, e.getMessage()); + logger.error("Error rolling dice for player: {}", player.orElse("anonymous"), e); + throw e; + } finally { + span.end(); + } + } + + private int rollSingleDie() { + Span span = tracer.spanBuilder("roll-single-die").setSpanKind(SpanKind.INTERNAL).startSpan(); + + try (Scope scope = span.makeCurrent()) { + // Simulate some work + try { + Thread.sleep(ThreadLocalRandom.current().nextInt(1, 5)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + int result = ThreadLocalRandom.current().nextInt(1, 7); + span.setAttribute("dice.value", result); + + // Simulate occasional errors (5% of the time) + if (ThreadLocalRandom.current().nextDouble() < 0.05) { + throw new RuntimeException("Simulated dice roll error"); + } + + return result; + } catch (Exception e) { + span.recordException(e); + span.setStatus(StatusCode.ERROR, e.getMessage()); + throw e; + } finally { + span.end(); + } + } + + @GetMapping("/health") + public Map health() { + Span span = tracer.spanBuilder("health-check").setSpanKind(SpanKind.SERVER).startSpan(); + + try (Scope scope = span.makeCurrent()) { + logger.info("Health check requested"); + + Map health = new HashMap<>(); + health.put("status", "UP"); + health.put("service", "dice-server"); + health.put("version", "1.0.0"); + + span.setAttribute("health.status", "UP"); + return health; + } finally { + span.end(); + } + } +} diff --git a/reference-application/src/main/resources/application.yml b/reference-application/src/main/resources/application.yml new file mode 100644 index 000000000..93dabb79e --- /dev/null +++ b/reference-application/src/main/resources/application.yml @@ -0,0 +1,38 @@ +# Application Configuration +server: + port: 8080 + +spring: + application: + name: dice-server + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: always + metrics: + enabled: true + +# Logging configuration +logging: + level: + io.opentelemetry.example: INFO + io.opentelemetry: WARN + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n" + +# OpenTelemetry Configuration (can be overridden by environment variables) +otel: + service: + name: dice-server + version: 1.0.0 + traces: + exporter: console + metrics: + exporter: console + logs: + exporter: console \ No newline at end of file diff --git a/reference-application/src/main/resources/logback-spring.xml b/reference-application/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..998d82fb1 --- /dev/null +++ b/reference-application/src/main/resources/logback-spring.xml @@ -0,0 +1,22 @@ + + + + + + + ${CONSOLE_LOG_PATTERN} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/reference-application/src/test/java/io/opentelemetry/example/ReferenceApplicationTests.java b/reference-application/src/test/java/io/opentelemetry/example/ReferenceApplicationTests.java new file mode 100644 index 000000000..8fc3cd901 --- /dev/null +++ b/reference-application/src/test/java/io/opentelemetry/example/ReferenceApplicationTests.java @@ -0,0 +1,76 @@ +package io.opentelemetry.example; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.ResponseEntity; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ReferenceApplicationTests { + + @LocalServerPort private int port; + + @Autowired private TestRestTemplate restTemplate; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testRollDice() throws Exception { + ResponseEntity response = + restTemplate.getForEntity("http://localhost:" + port + "/rolldice", String.class); + + assertEquals(200, response.getStatusCode().value()); + + JsonNode json = objectMapper.readTree(response.getBody()); + assertNotNull(json.get("result")); + assertNotNull(json.get("player")); + + int result = json.get("result").asInt(); + assertTrue(result >= 1 && result <= 6); + } + + @Test + void testRollDiceWithPlayer() throws Exception { + ResponseEntity response = + restTemplate.getForEntity( + "http://localhost:" + port + "/rolldice?player=testplayer", String.class); + + assertEquals(200, response.getStatusCode().value()); + + JsonNode json = objectMapper.readTree(response.getBody()); + assertEquals("testplayer", json.get("player").asText()); + } + + @Test + void testRollMultipleDice() throws Exception { + ResponseEntity response = + restTemplate.getForEntity("http://localhost:" + port + "/rolldice?rolls=3", String.class); + + assertEquals(200, response.getStatusCode().value()); + + JsonNode json = objectMapper.readTree(response.getBody()); + assertNotNull(json.get("results")); + assertNotNull(json.get("sum")); + assertEquals(3, json.get("results").size()); + } + + @Test + void testHealth() throws Exception { + ResponseEntity response = + restTemplate.getForEntity("http://localhost:" + port + "/health", String.class); + + assertEquals(200, response.getStatusCode().value()); + + JsonNode json = objectMapper.readTree(response.getBody()); + assertEquals("UP", json.get("status").asText()); + assertEquals("dice-server", json.get("service").asText()); + } +} diff --git a/reference-application/src/test/java/io/opentelemetry/example/TelemetryTest.java.todo b/reference-application/src/test/java/io/opentelemetry/example/TelemetryTest.java.todo new file mode 100644 index 000000000..dc05edb49 --- /dev/null +++ b/reference-application/src/test/java/io/opentelemetry/example/TelemetryTest.java.todo @@ -0,0 +1,322 @@ +package io.opentelemetry.example; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockserver.integration.ClientAndServer.startClientAndServer; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.stop.Stop.stopQuietly; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import com.google.protobuf.InvalidProtocolBufferException; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.trace.v1.Span; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.Body; +import org.mockserver.model.HttpRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; + +/** + * Test class to validate that the reference application properly exports telemetry data. This test + * uses MockServer to capture OTLP requests and verify that spans and metrics are being generated + * and exported correctly. + */ +@SpringBootTest( + classes = {ReferenceApplication.class}, + webEnvironment = RANDOM_PORT) +class TelemetryTest { + + @LocalServerPort private int port; + + @Autowired TestRestTemplate template; + + // Port of endpoint to export the telemetry data. Using a different port than 4318 + // to avoid conflicts with the Java Agent trying to connect to 4318 + static final int EXPORTER_ENDPOINT_PORT = 14318; + + // Server running to collect traces and metrics. The OpenTelemetry Java agent is configured + // to export telemetry with the http/protobuf protocol. + static ClientAndServer collectorServer; + + @BeforeAll + public static void setUp() { + collectorServer = startClientAndServer(EXPORTER_ENDPOINT_PORT); + collectorServer.when(request()).respond(response().withStatusCode(200)); + } + + @AfterAll + public static void tearDown() { + stopQuietly(collectorServer); + } + + @Test + public void testDiceRollTelemetry() { + // Call the dice roll endpoint + template.getForEntity(URI.create("http://localhost:" + port + "/rolldice"), String.class); + + // Verify telemetry data was exported + await() + .atMost(30, SECONDS) + .untilAsserted( + () -> { + var requests = collectorServer.retrieveRecordedRequests(request()); + + // Verify traces - should have HTTP spans and custom spans + var spans = extractSpansFromRequests(requests); + assertThat(spans) + .extracting(Span::getName) + .contains("GET /rolldice", "roll-dice", "roll-single-die"); + + // Verify metrics - should have custom dice roll metrics + var metrics = extractMetricsFromRequests(requests); + assertThat(metrics) + .extracting(Metric::getName) + .contains("dice_rolls_total", "dice_roll_duration_seconds"); + }); + } + + @Test + public void testFibonacciTelemetry() { + // Call the fibonacci endpoint + template.getForEntity(URI.create("http://localhost:" + port + "/fibonacci?n=8"), String.class); + + // Verify telemetry data was exported + await() + .atMost(30, SECONDS) + .untilAsserted( + () -> { + var requests = collectorServer.retrieveRecordedRequests(request()); + + // Verify traces - should have HTTP spans and custom fibonacci spans + var spans = extractSpansFromRequests(requests); + assertThat(spans) + .extracting(Span::getName) + .contains("GET /fibonacci", "calculate-fibonacci", "fibonacci-calculation"); + + // Verify metrics - should have custom fibonacci metrics + var metrics = extractMetricsFromRequests(requests); + assertThat(metrics) + .extracting(Metric::getName) + .contains("fibonacci_calculations_total", "fibonacci_duration_seconds"); + }); + } + + @Test + public void testMultipleDiceRollTelemetry() { + // Call the multiple dice roll endpoint + template.getForEntity( + URI.create("http://localhost:" + port + "/rolldice?rolls=3"), String.class); + + // Verify telemetry data was exported + await() + .atMost(30, SECONDS) + .untilAsserted( + () -> { + var requests = collectorServer.retrieveRecordedRequests(request()); + + // Verify traces - should have HTTP spans and custom spans for multiple rolls + var spans = extractSpansFromRequests(requests); + assertThat(spans) + .extracting(Span::getName) + .contains("GET /rolldice", "roll-dice", "roll-single-die"); + + // Check that there are multiple roll-single-die spans (one for each roll) + var singleDieSpans = + spans.stream() + .filter(span -> "roll-single-die".equals(span.getName())) + .collect(Collectors.toList()); + + assertThat(singleDieSpans).hasSizeGreaterThanOrEqualTo(3); + + // Verify span attributes contain dice information + var rollDiceSpans = + spans.stream() + .filter(span -> "roll-dice".equals(span.getName())) + .collect(Collectors.toList()); + + assertThat(rollDiceSpans).isNotEmpty(); + + // Verify span attributes contain roll information + rollDiceSpans.forEach( + span -> { + var attributes = span.getAttributesList(); + assertThat(attributes).isNotEmpty(); + }); + }); + } + + @Test + public void testHealthEndpointTelemetry() { + // Call the health endpoint + template.getForEntity(URI.create("http://localhost:" + port + "/health"), String.class); + + // Verify telemetry data was exported + await() + .atMost(30, SECONDS) + .untilAsserted( + () -> { + var requests = collectorServer.retrieveRecordedRequests(request()); + + // Verify traces - should have HTTP spans for health check + var spans = extractSpansFromRequests(requests); + assertThat(spans).extracting(Span::getName).contains("GET /health", "health-check"); + }); + } + + @Test + public void testSpanEventsAndAttributes() { + // Call the fibonacci endpoint with a larger number to trigger progress events + template.getForEntity(URI.create("http://localhost:" + port + "/fibonacci?n=25"), String.class); + + // Verify telemetry data includes events and detailed attributes + await() + .atMost(30, SECONDS) + .untilAsserted( + () -> { + var requests = collectorServer.retrieveRecordedRequests(request()); + + var spans = extractSpansFromRequests(requests); + + // Find the fibonacci calculation span + var fibonacciSpans = + spans.stream() + .filter(span -> "fibonacci-calculation".equals(span.getName())) + .collect(Collectors.toList()); + + assertThat(fibonacciSpans).isNotEmpty(); + + // Check that the span has attributes + fibonacciSpans.forEach( + span -> { + var attributes = span.getAttributesList(); + var hasNAttribute = + attributes.stream() + .anyMatch( + attr -> + "fibonacci.n".equals(attr.getKey()) + && attr.getValue().getIntValue() == 25); + assertThat(hasNAttribute).isTrue(); + }); + + // Find the calculate-fibonacci span and check for events + var calculateSpans = + spans.stream() + .filter(span -> "calculate-fibonacci".equals(span.getName())) + .collect(Collectors.toList()); + + assertThat(calculateSpans).isNotEmpty(); + + calculateSpans.forEach( + span -> { + // Should have fibonacci-calculated event + var events = span.getEventsList(); + var hasFibonacciEvent = + events.stream() + .anyMatch(event -> "fibonacci-calculated".equals(event.getName())); + assertThat(hasFibonacciEvent).isTrue(); + }); + }); + } + + @Test + public void testBaggagePropagation() { + // Call an endpoint that should propagate baggage + template.getForEntity( + URI.create("http://localhost:" + port + "/rolldice?player=testuser"), String.class); + + // Verify telemetry data was exported with baggage + await() + .atMost(30, SECONDS) + .untilAsserted( + () -> { + var requests = collectorServer.retrieveRecordedRequests(request()); + + // Verify traces contain baggage information + var spans = extractSpansFromRequests(requests); + assertThat(spans).extracting(Span::getName).contains("roll-dice"); + + // Check for spans with player information in attributes + var rollDiceSpans = + spans.stream() + .filter(span -> "roll-dice".equals(span.getName())) + .collect(Collectors.toList()); + + assertThat(rollDiceSpans).isNotEmpty(); + + // Verify span has player attribute + rollDiceSpans.forEach( + span -> { + var attributes = span.getAttributesList(); + var playerAttr = + attributes.stream() + .anyMatch( + attr -> + "dice.player".equals(attr.getKey()) + && "testuser".equals(attr.getValue().getStringValue())); + assertThat(playerAttr).isTrue(); + }); + }); + } + + /** + * Extract spans from http requests received by a telemetry collector. + * + * @param requests Request received by a http server trace collector + * @return spans extracted from the request body + */ + private List extractSpansFromRequests(HttpRequest[] requests) { + return Arrays.stream(requests) + .map(HttpRequest::getBody) + .flatMap(body -> getExportTraceServiceRequest(body).stream()) + .flatMap(r -> r.getResourceSpansList().stream()) + .flatMap(r -> r.getScopeSpansList().stream()) + .flatMap(r -> r.getSpansList().stream()) + .collect(Collectors.toList()); + } + + private Optional getExportTraceServiceRequest(Body body) { + try { + return Optional.ofNullable(ExportTraceServiceRequest.parseFrom(body.getRawBytes())); + } catch (InvalidProtocolBufferException e) { + return Optional.empty(); + } + } + + /** + * Extract metrics from http requests received by a telemetry collector. + * + * @param requests Request received by an http server telemetry collector + * @return metrics extracted from the request body + */ + private List extractMetricsFromRequests(HttpRequest[] requests) { + return Arrays.stream(requests) + .map(HttpRequest::getBody) + .flatMap(body -> getExportMetricsServiceRequest(body).stream()) + .flatMap(r -> r.getResourceMetricsList().stream()) + .flatMap(r -> r.getScopeMetricsList().stream()) + .flatMap(r -> r.getMetricsList().stream()) + .collect(Collectors.toList()); + } + + private Optional getExportMetricsServiceRequest(Body body) { + try { + return Optional.ofNullable(ExportMetricsServiceRequest.parseFrom(body.getRawBytes())); + } catch (InvalidProtocolBufferException e) { + return Optional.empty(); + } + } +} diff --git a/reference-application/test-e2e.sh b/reference-application/test-e2e.sh new file mode 100755 index 000000000..bd54677e0 --- /dev/null +++ b/reference-application/test-e2e.sh @@ -0,0 +1,247 @@ +#!/bin/bash + +# End-to-end test for the reference application using docker-compose +# This test verifies that the full stack works correctly with OpenTelemetry + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to wait for a service to be ready +wait_for_service() { + local url=$1 + local service_name=$2 + local max_attempts=30 + local attempt=1 + + print_status "Waiting for $service_name to be ready at $url..." + + while [ $attempt -le $max_attempts ]; do + if curl -sf "$url" > /dev/null 2>&1; then + print_status "$service_name is ready!" + return 0 + fi + + if [ $attempt -eq $max_attempts ]; then + print_error "$service_name failed to start within $max_attempts attempts" + return 1 + fi + + print_status "Attempt $attempt/$max_attempts: $service_name not ready yet, waiting..." + sleep 2 + ((attempt++)) + done +} + +# Function to test application endpoints +test_endpoints() { + print_status "Testing application endpoints..." + + # Test basic dice roll + print_status "Testing /rolldice endpoint..." + response=$(curl -sf http://localhost:8080/rolldice) + if echo "$response" | jq -e '.result' > /dev/null && echo "$response" | jq -e '.player' > /dev/null; then + print_status "✓ /rolldice endpoint working" + else + print_error "✗ /rolldice endpoint failed" + return 1 + fi + + # Test dice roll with player + print_status "Testing /rolldice?player=testuser endpoint..." + response=$(curl -sf "http://localhost:8080/rolldice?player=testuser") + if echo "$response" | jq -r '.player' | grep -q "testuser"; then + print_status "✓ /rolldice with player working" + else + print_error "✗ /rolldice with player failed" + return 1 + fi + + # Test health endpoint + print_status "Testing /health endpoint..." + response=$(curl -sf http://localhost:8080/health) + if echo "$response" | jq -r '.status' | grep -q "UP"; then + print_status "✓ /health endpoint working" + else + print_error "✗ /health endpoint failed" + return 1 + fi + + # Test metrics endpoint + print_status "Testing /actuator/prometheus endpoint..." + if curl -sf http://localhost:8080/actuator/prometheus | grep -q "dice_rolls_total"; then + print_status "✓ /actuator/prometheus endpoint working with custom metrics" + else + print_error "✗ /actuator/prometheus endpoint failed or missing custom metrics" + return 1 + fi + + print_status "All endpoint tests passed!" +} + +# Function to test OpenTelemetry collector +test_collector() { + print_status "Testing OpenTelemetry collector..." + + # Check collector health endpoint + if curl -sf http://localhost:13133 > /dev/null; then + print_status "✓ OpenTelemetry collector health endpoint is accessible" + else + print_warning "! OpenTelemetry collector health endpoint not accessible (this might be expected)" + fi + + # Check if collector is receiving and processing data by examining logs + print_status "Checking collector logs for telemetry data processing..." + + # Generate some telemetry data + curl -sf http://localhost:8080/rolldice > /dev/null + curl -sf http://localhost:8080/rolldice?rolls=3 > /dev/null + + # Wait a bit for data to be processed + sleep 5 + + # Check collector logs for evidence of data processing + if $compose_cmd logs otel-collector 2>/dev/null | grep -q -E "(spans|metrics|logs).*processed"; then + print_status "✓ OpenTelemetry collector is processing telemetry data" + else + print_warning "! Could not verify telemetry data processing in collector logs" + fi +} + +# Function to test Prometheus integration +test_prometheus() { + print_status "Testing Prometheus integration..." + + # Wait for Prometheus to be ready + if wait_for_service "http://localhost:9090/-/ready" "Prometheus"; then + print_status "✓ Prometheus is running" + + # Check if Prometheus can scrape metrics from the collector + if curl -sf "http://localhost:9090/api/v1/targets" | jq -r '.data.activeTargets[].health' | grep -q "up"; then + print_status "✓ Prometheus has healthy targets" + else + print_warning "! Prometheus targets may not be healthy" + fi + else + print_warning "! Prometheus failed to start" + fi +} + +# Function to get the docker compose command +get_docker_compose_cmd() { + if command -v "docker-compose" &> /dev/null; then + echo "docker-compose" + elif docker compose version &> /dev/null; then + echo "docker compose" + else + return 1 + fi +} + +# Function to cleanup resources +cleanup() { + print_status "Cleaning up resources..." + local compose_cmd + if compose_cmd=$(get_docker_compose_cmd); then + $compose_cmd down --volumes --remove-orphans || true + fi + + # Clean up any dangling resources + docker system prune -f || true +} + +# Main execution +main() { + print_status "Starting end-to-end test for OpenTelemetry Reference Application" + + # Handle dry-run mode for testing + if [[ "${1:-}" == "--dry-run" ]]; then + print_status "Running in dry-run mode - skipping actual Docker operations" + print_status "✅ Script validation passed" + return 0 + fi + + # Ensure we're in the right directory + if [[ ! -f "docker-compose.yml" ]]; then + print_error "docker-compose.yml not found. Please run this script from the reference-application directory." + exit 1 + fi + + # Ensure required tools are available + for tool in docker curl jq; do + if ! command -v "$tool" &> /dev/null; then + print_error "$tool is required but not installed." + exit 1 + fi + done + + # Check for docker compose command + if ! compose_cmd=$(get_docker_compose_cmd); then + print_error "docker-compose or 'docker compose' is required but not available." + exit 1 + fi + + print_status "Using Docker Compose command: $compose_cmd" + + # Build and start services + print_status "Building and starting services with docker-compose..." + $compose_cmd down --volumes --remove-orphans || true + $compose_cmd up --build -d + + # Wait for services to be ready + if ! wait_for_service "http://localhost:8080/health" "Reference Application"; then + print_error "Reference application failed to start" + cleanup + exit 1 + fi + + if ! wait_for_service "http://localhost:4318/v1/traces" "OpenTelemetry Collector OTLP HTTP"; then + print_warning "OpenTelemetry Collector OTLP HTTP endpoint not accessible" + fi + + # Run tests + if test_endpoints; then + print_status "✅ Application endpoint tests passed" + else + print_error "❌ Application endpoint tests failed" + cleanup + exit 1 + fi + + test_collector + test_prometheus + + print_status "🎉 End-to-end test completed successfully!" + print_status "The reference application is working correctly with OpenTelemetry stack" + + # Cleanup + cleanup + + print_status "✅ All tests passed and cleanup completed" +} + +# Trap to ensure cleanup on script exit +trap cleanup EXIT + +# Run main function +main "$@" \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 30c4a7b2d..7e682f284 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,7 @@ include( ":opentelemetry-examples-micrometer-shim", ":opentelemetry-examples-otlp", ":opentelemetry-examples-prometheus", + ":opentelemetry-examples-reference-application", ":opentelemetry-examples-sdk-usage", ":opentelemetry-examples-telemetry-testing", ":opentelemetry-examples-zipkin",