diff --git a/.github/workflows/oats-tests.yml b/.github/workflows/oats-tests.yml new file mode 100644 index 000000000..ec78046eb --- /dev/null +++ b/.github/workflows/oats-tests.yml @@ -0,0 +1,38 @@ +name: OATS Tests + +on: + pull_request: + branches: + - main + paths: + - .mise.toml + - .github/workflows/oats-tests.yml + - cel-sampler/** + - .mise/tasks/oats-tests.sh + workflow_dispatch: + +permissions: + contents: read + +jobs: + acceptance-tests: + runs-on: ubuntu-24.04 + steps: + - name: Check out + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up JDK for running Gradle + uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 + with: + distribution: temurin + java-version: 17 + + - name: Set up gradle + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 + with: + cache-read-only: ${{ github.event_name == 'pull_request' }} + + - uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1 + + - name: Run OATS tests + run: mise run oats-tests diff --git a/.mise/tasks/oats-tests.sh b/.mise/tasks/oats-tests.sh new file mode 100755 index 000000000..c9c760087 --- /dev/null +++ b/.mise/tasks/oats-tests.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +#MISE description="Run OATS tests for cel-sampler" + +set -euo pipefail + +echo "==> Building cel-sampler shadow JAR..." +./gradlew :cel-sampler:shadowJar + +echo "==> Building test application..." +# testapp is a standalone Gradle project, needs to be built separately +(cd cel-sampler/testapp && ../../gradlew jar) + +echo "==> Running OATS integration tests..." +(cd cel-sampler && oats -timeout 5m oats/oats.yaml) diff --git a/cel-sampler/build.gradle.kts b/cel-sampler/build.gradle.kts index bff7292da..24e6621e7 100644 --- a/cel-sampler/build.gradle.kts +++ b/cel-sampler/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("otel.java-conventions") id("otel.publish-conventions") + id("com.gradleup.shadow") } description = "Sampler which makes its decision based on semantic attributes values" @@ -18,3 +19,32 @@ dependencies { testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") } + +tasks { + shadowJar { + /** + * Shaded version of this extension is required when using it as a OpenTelemetry Java Agent + * extension. Shading bundles the dependencies required by this extension in the resulting JAR, + * ensuring their presence on the classpath at runtime. + * + * See http://gradleup.com/shadow/introduction/#introduction for reference. + */ + archiveClassifier.set("shadow") + } + + jar { + /** + * We need to publish both - shaded and unshaded variants of the dependency + * Shaded dependency is required for use with the Java agent. + * Unshaded dependency can be used with OTel Autoconfigure module. + * + * Not overriding the classifier to empty results in an implicit classifier 'plain' being + * used with the standard JAR. + */ + archiveClassifier.set("") + } + + assemble { + dependsOn(shadowJar) + } +} diff --git a/cel-sampler/gradle.properties b/cel-sampler/gradle.properties new file mode 100644 index 000000000..52b19f4e6 --- /dev/null +++ b/cel-sampler/gradle.properties @@ -0,0 +1,2 @@ +# TODO: uncomment when ready to mark as stable +# otel.stable=true \ No newline at end of file diff --git a/cel-sampler/oats/Dockerfile b/cel-sampler/oats/Dockerfile new file mode 100644 index 000000000..4b6be4d1e --- /dev/null +++ b/cel-sampler/oats/Dockerfile @@ -0,0 +1,24 @@ +FROM eclipse-temurin:21-jre + +WORKDIR /usr/src/app/ + +# renovate: datasource=github-releases depName=open-telemetry/opentelemetry-java-instrumentation +ENV OPENTELEMETRY_JAVA_INSTRUMENTATION_VERSION=v2.22.0 + +COPY ./cel-sampler/testapp/build/libs/testapp.jar ./app.jar + +# Add the CEL sampler extension JAR +COPY ./cel-sampler/build/libs/*-shadow.jar ./extensions/ + +COPY ./cel-sampler/oats/otel-config.yaml ./otel-config.yaml + +ADD --chmod=644 https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/$OPENTELEMETRY_JAVA_INSTRUMENTATION_VERSION/opentelemetry-javaagent.jar ./opentelemetry-javaagent.jar + +# Configure the Java agent with the CEL sampler extension +ENV JAVA_TOOL_OPTIONS=-javaagent:./opentelemetry-javaagent.jar +ENV OTEL_JAVAAGENT_EXTENSIONS=/usr/src/app/extensions/ + +ENV OTEL_EXPERIMENTAL_CONFIG_FILE=./otel-config.yaml + +EXPOSE 8080 +ENTRYPOINT [ "java", "-jar", "./app.jar" ] diff --git a/cel-sampler/oats/README.md b/cel-sampler/oats/README.md new file mode 100644 index 000000000..5fd33c5ca --- /dev/null +++ b/cel-sampler/oats/README.md @@ -0,0 +1,25 @@ +# CEL Sampler OATS Integration Tests + +This directory contains acceptance tests for the CEL-based sampler extension using the +[OATS (OpenTelemetry Acceptance Test Suite)](https://github.com/grafana/oats) framework. + +## Overview + +These tests verify that the CEL sampler correctly: + +- Drops traces for health check endpoints (`/healthcheck`, `/metrics`) +- Samples traces for regular API endpoints (`/hello`, `/api/data`) +- Loads correctly as a Java agent extension +- Works with declarative configuration + +## Running the Tests + +You can build all assets and run tests using: + +`mise run oats-test` + +Or manually run just the tests (from the `cel-sampler` directory): + +```bash +~/go/bin/oats oats/oats.yaml +``` diff --git a/cel-sampler/oats/docker-compose.yml b/cel-sampler/oats/docker-compose.yml new file mode 100644 index 000000000..bfce06138 --- /dev/null +++ b/cel-sampler/oats/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3' +services: + app: + build: + context: ../../ + dockerfile: cel-sampler/oats/Dockerfile + environment: + OTEL_SERVICE_NAME: "cel-sampler-test-app" + OTEL_EXPORTER_OTLP_ENDPOINT: http://lgtm:4318 + OTEL_EXPERIMENTAL_CONFIG_FILE: ./otel-config.yaml + ports: + - "8080:8080" diff --git a/cel-sampler/oats/oats.yaml b/cel-sampler/oats/oats.yaml new file mode 100644 index 000000000..2d518ceb4 --- /dev/null +++ b/cel-sampler/oats/oats.yaml @@ -0,0 +1,34 @@ +# OATS is an acceptance testing framework for OpenTelemetry - https://github.com/grafana/oats +oats-schema-version: 2 + +docker-compose: + files: + - ./docker-compose.yml + +input: + - path: /hello + - path: /api/data + - path: /healthcheck + - path: /metrics + +expected: + traces: + # Verify that /hello and /api/data endpoints are sampled + - traceql: '{ span.http.route = "/hello" }' + equals: "GET /hello" + attributes: + http.request.method: "GET" + http.route: "/hello" + - traceql: '{ span.http.route = "/api/data" }' + equals: "GET /api/data" + attributes: + http.request.method: "GET" + http.response.status_code: 200 + + # Verify that health check and metrics endpoints are dropped: + - traceql: '{ span.http.route = "/healthcheck" }' + count: + max: 0 + - traceql: '{ span.http.route = "/metrics" }' + count: + max: 0 diff --git a/cel-sampler/oats/otel-config.yaml b/cel-sampler/oats/otel-config.yaml new file mode 100644 index 000000000..d62f75a39 --- /dev/null +++ b/cel-sampler/oats/otel-config.yaml @@ -0,0 +1,27 @@ +# OpenTelemetry Declarative Configuration for CEL-based Sampler Integration Test + +file_format: "1.0-rc.2" + +resource: + attributes: + - name: service.name + value: cel-sampler-test + +tracer_provider: + processors: + - batch: + exporter: + otlp_http: + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/traces + sampler: + parent_based: + root: + cel_based: + expressions: + - expression: 'attribute["url.path"] == "/healthcheck"' + action: DROP + - expression: 'attribute["url.path"] == "/metrics"' + action: DROP + # Fallback sampler: sample everything else (including /api/ endpoints) + fallback_sampler: + always_on: {} diff --git a/cel-sampler/testapp/.gitignore b/cel-sampler/testapp/.gitignore new file mode 100644 index 000000000..f7ab597e6 --- /dev/null +++ b/cel-sampler/testapp/.gitignore @@ -0,0 +1,2 @@ +build/ +.gradle/ \ No newline at end of file diff --git a/cel-sampler/testapp/build.gradle.kts b/cel-sampler/testapp/build.gradle.kts new file mode 100644 index 000000000..a9ec0dbb5 --- /dev/null +++ b/cel-sampler/testapp/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + java + application +} + +application { + mainClass.set("io.opentelemetry.contrib.sampler.cel.testapp.SimpleServer") +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "io.opentelemetry.contrib.sampler.cel.testapp.SimpleServer" + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) +} \ No newline at end of file diff --git a/cel-sampler/testapp/settings.gradle.kts b/cel-sampler/testapp/settings.gradle.kts new file mode 100644 index 000000000..07325113d --- /dev/null +++ b/cel-sampler/testapp/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "testapp" \ No newline at end of file diff --git a/cel-sampler/testapp/src/main/java/io/opentelemetry/contrib/sampler/cel/testapp/SimpleServer.java b/cel-sampler/testapp/src/main/java/io/opentelemetry/contrib/sampler/cel/testapp/SimpleServer.java new file mode 100644 index 000000000..e1dbddbf4 --- /dev/null +++ b/cel-sampler/testapp/src/main/java/io/opentelemetry/contrib/sampler/cel/testapp/SimpleServer.java @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sampler.cel.testapp; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; + +/** + * Simple HTTP server for testing CEL-based sampler extension. + * + *
This application provides multiple endpoints to test different sampling behaviors: + * - /api/data - Should be sampled (API endpoint) + * - /healthcheck - Should be dropped (health check endpoint) + * - /metrics - Should be dropped (metrics endpoint) + * - /hello - Should be sampled (regular endpoint) + */ +public class SimpleServer { + + private static final Logger logger = Logger.getLogger(SimpleServer.class.getName()); + + public static void main(String[] args) throws Exception { + int port = 8080; + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + + // Regular endpoints that should be sampled + server.createContext("/hello", new ResponseHandler("Hello from test app!")); + server.createContext("/api/data", new ResponseHandler("API data response")); + + // Health/monitoring endpoints that should be dropped + server.createContext("/healthcheck", new ResponseHandler("OK")); + server.createContext("/metrics", new ResponseHandler("metrics=1")); + + server.setExecutor(null); + + logger.info("Starting server on port " + port); + server.start(); + } + + static class ResponseHandler implements HttpHandler { + private final String response; + + ResponseHandler(String response) { + this.response = response; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8); + + exchange.sendResponseHeaders(200, responseBytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(responseBytes); + } + + logger.info("Handled request to " + exchange.getRequestURI().getPath()); + } + } +} \ No newline at end of file diff --git a/mise.toml b/mise.toml index fe30e02a7..42c985b28 100644 --- a/mise.toml +++ b/mise.toml @@ -1,6 +1,7 @@ [tools] lychee = "0.22.0" markdownlint-cli2 = "0.20.0" +"go:github.com/grafana/oats" = "0.6.0" [settings] # Only install tools explicitly defined in the [tools] section above @@ -10,4 +11,5 @@ idiomatic_version_file_enable_tools = [] # Based on: https://github.com/jdx/mise/discussions/4461 windows_executable_extensions = ["sh"] windows_default_file_shell_args = "bash" +unix_default_file_shell_args = "bash" use_file_shell_for_executable_tasks = true