Add boot42-otel module: Spring Boot 4.2.0-RC1 OpenTelemetry conventions, shared OTLP settings and MDC keys vs 4.1.2
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# boot42-otel
|
||||
|
||||
Companion module for the ankurm.com article *Spring Boot 4.2 OpenTelemetry: Semantic Conventions, Shared OTLP Settings and MDC Keys*.
|
||||
Everything runs against **Spring Boot 4.2.0-RC1** (a release candidate, **not GA**) and against **4.1.2** (GA) as the baseline. JDK 25.
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `pom.xml` / `pom-41.xml` | the same project on 4.2.0-RC1 and on 4.1.2 |
|
||||
| `src/main/.../OtelApplication` | one endpoint, `GET /hello/{name}`, that logs a line |
|
||||
| `src/test/.../OtlpSink` | a 60-line OTLP/HTTP receiver: records path, headers and payload strings of every export request |
|
||||
| `src/test/.../ConventionsTest` | meters and span names for each `management.observations.conventions` value, including the start-up failure RC1 gives for `open-telemetry` |
|
||||
| `src/test/.../ConventionSpellingsTest` | seven spellings of the conventions value, each started for real: which start and which meter they produce |
|
||||
| `src/test/.../SharedOtlpTest` | `management.opentelemetry.otlp.*`: paths, merged headers, gzip, and signal-specific overrides |
|
||||
| `src/test/.../MdcAndLongTaskTimerTest` | custom MDC keys, and the active-observation LongTaskTimer that is no longer on by default |
|
||||
| `src/test/.../PrometheusNamesTest` | the `http_server_*` names a dashboard query sees under each convention |
|
||||
| `output/` | every transcript quoted in the article; `./scripts/run-all.sh` regenerates them |
|
||||
|
||||
Run: `./scripts/run-all.sh`. `ConventionsTest` pins the RC1 behaviour on purpose; when a later build changes it, that test fails and the article needs updating.
|
||||
|
||||
There is deliberately no `docs/` folder: the explanations live in the article.
|
||||
@@ -0,0 +1,37 @@
|
||||
# 01-conventions-metrics-and-spans (Spring Boot 4.1.2, JDK 25)
|
||||
|
||||
== management.observations.conventions=(property not set)
|
||||
-- meters whose name contains 'http':
|
||||
http.server.requests [error=none, exception=none, method=GET, outcome=SUCCESS, status=200, uri=/hello/{name}]
|
||||
http.server.requests.active [exception=none, method=GET, outcome=SUCCESS, status=200, uri=UNKNOWN]
|
||||
-- span-related strings in the OTLP trace payload (1 request(s)):
|
||||
/hello/ankurJ
|
||||
/hello/{name}z
|
||||
http get /hello/{name}0
|
||||
|
||||
== management.observations.conventions=micrometer
|
||||
-- meters whose name contains 'http':
|
||||
http.server.requests [error=none, exception=none, method=GET, outcome=SUCCESS, status=200, uri=/hello/{name}]
|
||||
http.server.requests.active [exception=none, method=GET, outcome=SUCCESS, status=200, uri=UNKNOWN]
|
||||
-- span-related strings in the OTLP trace payload (1 request(s)):
|
||||
/hello/ankurJ
|
||||
/hello/{name}z
|
||||
http get /hello/{name}0
|
||||
|
||||
== management.observations.conventions=open-telemetry
|
||||
-- meters whose name contains 'http':
|
||||
http.server.requests [error=none, exception=none, method=GET, outcome=SUCCESS, status=200, uri=/hello/{name}]
|
||||
http.server.requests.active [exception=none, method=GET, outcome=SUCCESS, status=200, uri=UNKNOWN]
|
||||
-- span-related strings in the OTLP trace payload (1 request(s)):
|
||||
/hello/ankurJ
|
||||
/hello/{name}z
|
||||
http get /hello/{name}0
|
||||
|
||||
== management.observations.conventions=opentelemetry
|
||||
-- meters whose name contains 'http':
|
||||
http.server.requests [error=none, exception=none, method=GET, outcome=SUCCESS, status=200, uri=/hello/{name}]
|
||||
http.server.requests.active [exception=none, method=GET, outcome=SUCCESS, status=200, uri=UNKNOWN]
|
||||
-- span-related strings in the OTLP trace payload (1 request(s)):
|
||||
/hello/ankurJ
|
||||
/hello/{name}z
|
||||
http get /hello/{name}0
|
||||
@@ -0,0 +1,29 @@
|
||||
# 01-conventions-metrics-and-spans (Spring Boot 4.2.0-RC1, JDK 25)
|
||||
|
||||
== management.observations.conventions=(property not set)
|
||||
-- meters whose name contains 'http':
|
||||
http.server.requests [error=none, exception=none, method=GET, outcome=SUCCESS, status=200, uri=/hello/{name}]
|
||||
-- span-related strings in the OTLP trace payload (1 request(s)):
|
||||
/hello/ankurJ
|
||||
/hello/{name}z
|
||||
http get /hello/{name}0
|
||||
|
||||
== management.observations.conventions=micrometer
|
||||
-- meters whose name contains 'http':
|
||||
http.server.requests [error=none, exception=none, method=GET, outcome=SUCCESS, status=200, uri=/hello/{name}]
|
||||
-- span-related strings in the OTLP trace payload (1 request(s)):
|
||||
/hello/ankurJ
|
||||
/hello/{name}z
|
||||
http get /hello/{name}0
|
||||
|
||||
== management.observations.conventions=open-telemetry
|
||||
-- application FAILED to start
|
||||
NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.http.server.observation.ServerRequestObservationConvention' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
|
||||
|
||||
== management.observations.conventions=opentelemetry
|
||||
-- meters whose name contains 'http':
|
||||
http.server.request.duration [error.type=none, error=none, http.request.method=GET, http.response.status_code=200, http.route=/hello/{name}, outcome=SUCCESS, url.scheme=http]
|
||||
-- span-related strings in the OTLP trace payload (1 request(s)):
|
||||
/hello/ankurJ
|
||||
/hello/{name}J
|
||||
GET /hello/{name}0
|
||||
@@ -0,0 +1,7 @@
|
||||
# 02-shared-otlp-endpoint (Spring Boot 4.1.2, JDK 25)
|
||||
management.opentelemetry.otlp.endpoint = <sink> (one property, no per-signal URLs)
|
||||
management.opentelemetry.otlp.headers = Authorization: Bearer shared-token
|
||||
management.opentelemetry.otlp.compression = gzip
|
||||
|
||||
requests that reached the sink:
|
||||
(none: 4.1.2 ignores the shared properties and exports to its default localhost:4318)
|
||||
@@ -0,0 +1,8 @@
|
||||
# 02-shared-otlp-endpoint (Spring Boot 4.2.0-RC1, JDK 25)
|
||||
management.opentelemetry.otlp.endpoint = <sink> (one property, no per-signal URLs)
|
||||
management.opentelemetry.otlp.headers = Authorization: Bearer shared-token
|
||||
management.opentelemetry.otlp.compression = gzip
|
||||
|
||||
requests that reached the sink:
|
||||
POST /v1/metrics Authorization=Bearer shared-token Content-Encoding=gzip
|
||||
POST /v1/traces Authorization=Bearer shared-token Content-Encoding=gzip
|
||||
@@ -0,0 +1,9 @@
|
||||
# 03-signal-specific-overrides-shared (Spring Boot 4.1.2, JDK 25)
|
||||
shared endpoint = <sink>
|
||||
tracing.export.otlp.endpoint = <sink>/custom/spans
|
||||
shared header Authorization = Bearer shared-token
|
||||
shared header X-Shared = shared-only
|
||||
tracing.export.otlp.headers.Authorization = Bearer traces-token
|
||||
|
||||
requests that reached the sink:
|
||||
POST /custom/spans Authorization=Bearer traces-token X-Shared=null
|
||||
@@ -0,0 +1,10 @@
|
||||
# 03-signal-specific-overrides-shared (Spring Boot 4.2.0-RC1, JDK 25)
|
||||
shared endpoint = <sink>
|
||||
tracing.export.otlp.endpoint = <sink>/custom/spans
|
||||
shared header Authorization = Bearer shared-token
|
||||
shared header X-Shared = shared-only
|
||||
tracing.export.otlp.headers.Authorization = Bearer traces-token
|
||||
|
||||
requests that reached the sink:
|
||||
POST /custom/spans Authorization=Bearer traces-token X-Shared=shared-only
|
||||
POST /v1/metrics Authorization=Bearer shared-token X-Shared=shared-only
|
||||
@@ -0,0 +1,8 @@
|
||||
# 04-mdc-keys (Spring Boot 4.1.2, JDK 25)
|
||||
management.tracing.mdc.trace-id-key = trace_id
|
||||
management.tracing.mdc.span-id-key = span_id
|
||||
log pattern correlation part reads both MDC keys
|
||||
|
||||
log line: INFO 4192 --- [boot42-otel] [o-auto-1-exec-1] default[<32-hex-trace-id>] custom[] c.a.o.OtelApplication$HelloController : handling hello for ankur
|
||||
default key traceId populated: true
|
||||
custom key trace_id populated: false
|
||||
@@ -0,0 +1,8 @@
|
||||
# 04-mdc-keys (Spring Boot 4.2.0-RC1, JDK 25)
|
||||
management.tracing.mdc.trace-id-key = trace_id
|
||||
management.tracing.mdc.span-id-key = span_id
|
||||
log pattern correlation part reads both MDC keys
|
||||
|
||||
log line: INFO 3810 --- [boot42-otel] [o-auto-1-exec-1] default[] custom[<32-hex-trace-id>] c.a.o.OtelApplication$HelloController : handling hello for ankur
|
||||
default key traceId populated: false
|
||||
custom key trace_id populated: true
|
||||
@@ -0,0 +1,11 @@
|
||||
# 05-active-long-task-timer (Spring Boot 4.1.2, JDK 25)
|
||||
|
||||
== (default)
|
||||
http.server.requests (TIMER)
|
||||
http.server.requests.active (LONG_TASK_TIMER)
|
||||
http.server.requests.active present: true
|
||||
|
||||
== management.metrics.observations.include-active-long-task-timer=true
|
||||
http.server.requests (TIMER)
|
||||
http.server.requests.active (LONG_TASK_TIMER)
|
||||
http.server.requests.active present: true
|
||||
@@ -0,0 +1,10 @@
|
||||
# 05-active-long-task-timer (Spring Boot 4.2.0-RC1, JDK 25)
|
||||
|
||||
== (default)
|
||||
http.server.requests (TIMER)
|
||||
http.server.requests.active present: false
|
||||
|
||||
== management.metrics.observations.include-active-long-task-timer=true
|
||||
http.server.requests (TIMER)
|
||||
http.server.requests.active (LONG_TASK_TIMER)
|
||||
http.server.requests.active present: true
|
||||
@@ -0,0 +1,19 @@
|
||||
# 06-prometheus-names (Spring Boot 4.1.2, JDK 25)
|
||||
|
||||
== management.observations.conventions=micrometer
|
||||
http_server_requests_active_seconds_count
|
||||
http_server_requests_active_seconds_max
|
||||
http_server_requests_active_seconds_sum
|
||||
http_server_requests_seconds_count
|
||||
http_server_requests_seconds_max
|
||||
http_server_requests_seconds_sum
|
||||
sample: http_server_requests_active_seconds_count{exception="none",method="GET",outcome="SUCCESS",status="200",uri="UNKNOWN"}
|
||||
|
||||
== management.observations.conventions=opentelemetry
|
||||
http_server_requests_active_seconds_count
|
||||
http_server_requests_active_seconds_max
|
||||
http_server_requests_active_seconds_sum
|
||||
http_server_requests_seconds_count
|
||||
http_server_requests_seconds_max
|
||||
http_server_requests_seconds_sum
|
||||
sample: http_server_requests_active_seconds_count{exception="none",method="GET",outcome="SUCCESS",status="200",uri="UNKNOWN"}
|
||||
@@ -0,0 +1,13 @@
|
||||
# 06-prometheus-names (Spring Boot 4.2.0-RC1, JDK 25)
|
||||
|
||||
== management.observations.conventions=micrometer
|
||||
http_server_requests_seconds_count
|
||||
http_server_requests_seconds_max
|
||||
http_server_requests_seconds_sum
|
||||
sample: http_server_requests_seconds_count{error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/hello/{name}"}
|
||||
|
||||
== management.observations.conventions=opentelemetry
|
||||
http_server_request_duration_seconds_count
|
||||
http_server_request_duration_seconds_max
|
||||
http_server_request_duration_seconds_sum
|
||||
sample: http_server_request_duration_seconds_count{error="none",error_type="none",http_request_method="GET",http_response_status_code="200",http_route="/hello/{name}",outcome="SUCCESS",url_scheme="http"}
|
||||
@@ -0,0 +1,8 @@
|
||||
# 07-conventions-spellings (Spring Boot 4.1.2, JDK 25)
|
||||
open-telemetry -> starts, server meter: [http.server.requests]
|
||||
open_telemetry -> starts, server meter: [http.server.requests]
|
||||
OPEN_TELEMETRY -> starts, server meter: [http.server.requests]
|
||||
opentelemetry -> starts, server meter: [http.server.requests]
|
||||
openTelemetry -> starts, server meter: [http.server.requests]
|
||||
micrometer -> starts, server meter: [http.server.requests]
|
||||
MICROMETER -> starts, server meter: [http.server.requests]
|
||||
@@ -0,0 +1,8 @@
|
||||
# 07-conventions-spellings (Spring Boot 4.2.0-RC1, JDK 25)
|
||||
open-telemetry -> FAILS to start
|
||||
open_telemetry -> starts, server meter: [http.server.request.duration]
|
||||
OPEN_TELEMETRY -> starts, server meter: [http.server.request.duration]
|
||||
opentelemetry -> starts, server meter: [http.server.request.duration]
|
||||
openTelemetry -> starts, server meter: [http.server.request.duration]
|
||||
micrometer -> starts, server meter: [http.server.requests]
|
||||
MICROMETER -> starts, server meter: [http.server.requests]
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.1.2</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>boot42-otel</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>boot42-otel</name>
|
||||
<description>Spring Boot 4.2 OpenTelemetry semantic conventions, shared OTLP settings and MDC keys, run against 4.2.0-RC1 and 4.1.2</description>
|
||||
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
<boot.label>4.1.2</boot.label>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-opentelemetry</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<boot.label>${boot.label}</boot.label>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.2.0-RC1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>boot42-otel</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>boot42-otel</name>
|
||||
<description>Spring Boot 4.2 OpenTelemetry semantic conventions, shared OTLP settings and MDC keys, run against 4.2.0-RC1 and 4.1.2</description>
|
||||
|
||||
<properties>
|
||||
<java.version>25</java.version>
|
||||
<boot.label>4.2.0-RC1</boot.label>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-opentelemetry</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<boot.label>${boot.label}</boot.label>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates every file in output/ on Spring Boot 4.2.0-RC1 and on the 4.1.2 baseline.
|
||||
# Needs JDK 25 and Maven 3.9 on PATH. Timing-sensitive lines are not captured on purpose.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
mvn -B -q test
|
||||
mvn -B -q -f pom-41.xml test
|
||||
echo "done: $(ls output | wc -l) files in output/"
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ankurm.otel;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@SpringBootApplication
|
||||
public class OtelApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(OtelApplication.class, args);
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class HelloController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(HelloController.class);
|
||||
|
||||
// The explicit name avoids the -parameters trap on standalone reactors.
|
||||
@GetMapping("/hello/{name}")
|
||||
String hello(@PathVariable("name") String name) {
|
||||
log.info("handling hello for {}", name);
|
||||
return "hello " + name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
spring.application.name=boot42-otel
|
||||
management.endpoints.web.exposure.include=health,metrics,prometheus
|
||||
management.tracing.sampling.probability=1.0
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.ankurm.otel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/** Starts the demo application on a random port with the given properties. */
|
||||
final class Apps {
|
||||
|
||||
private Apps() {
|
||||
}
|
||||
|
||||
static ConfigurableApplicationContext start(String... properties) {
|
||||
List<String> props = new ArrayList<>(List.of("server.port=0", "spring.main.banner-mode=off"));
|
||||
props.addAll(List.of(properties));
|
||||
return new SpringApplicationBuilder(OtelApplication.class).web(WebApplicationType.SERVLET)
|
||||
.properties(props.toArray(String[]::new))
|
||||
.run();
|
||||
}
|
||||
|
||||
static String call(ConfigurableApplicationContext ctx, String path) {
|
||||
String port = ctx.getEnvironment().getProperty("local.server.port");
|
||||
return RestClient.create().get().uri("http://localhost:" + port + path).retrieve().body(String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The server observation stops after the response bytes are on the wire, so a client that
|
||||
* reads the registry immediately can win the race and see nothing. Poll for up to 5 seconds.
|
||||
*/
|
||||
static void awaitHttpMeter(MeterRegistry registry) throws InterruptedException {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
if (registry.getMeters().stream().anyMatch(m -> m.getId().getName().startsWith("http.server.request")
|
||||
&& !m.getId().getName().endsWith(".active"))) {
|
||||
return;
|
||||
}
|
||||
Thread.sleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
/** One line per meter whose name mentions http: name, then sorted tag key=value pairs. */
|
||||
static List<String> httpMeters(MeterRegistry registry) {
|
||||
TreeSet<String> out = new TreeSet<>();
|
||||
for (Meter m : registry.getMeters()) {
|
||||
String name = m.getId().getName();
|
||||
if (name.contains("http")) {
|
||||
List<String> tags = new ArrayList<>();
|
||||
for (Tag t : m.getId().getTags()) {
|
||||
tags.add(t.getKey() + "=" + t.getValue());
|
||||
}
|
||||
tags.sort(String::compareTo);
|
||||
out.add(name + " " + tags);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(out);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ankurm.otel;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* The reference documentation says management.observations.conventions=open-telemetry. This
|
||||
* starts the application once per spelling and records whether it starts and which server HTTP
|
||||
* meter it ends up with. On 4.2.0-RC1 the documented spelling does not start.
|
||||
*/
|
||||
class ConventionSpellingsTest {
|
||||
|
||||
@Test
|
||||
void spellings() throws Exception {
|
||||
Transcript t = new Transcript("07-conventions-spellings");
|
||||
Map<String, String> outcome = new LinkedHashMap<>();
|
||||
for (String value : List.of("open-telemetry", "open_telemetry", "OPEN_TELEMETRY", "opentelemetry",
|
||||
"openTelemetry", "micrometer", "MICROMETER")) {
|
||||
String result;
|
||||
try {
|
||||
ConfigurableApplicationContext ctx = Apps.start("management.observations.conventions=" + value,
|
||||
"management.otlp.metrics.export.enabled=false");
|
||||
Apps.call(ctx, "/hello/x");
|
||||
MeterRegistry registry = ctx.getBean(MeterRegistry.class);
|
||||
Apps.awaitHttpMeter(registry);
|
||||
result = "starts, server meter: " + registry.getMeters().stream()
|
||||
.map(m -> m.getId().getName())
|
||||
.filter(n -> n.startsWith("http.server.request") && !n.endsWith(".active"))
|
||||
.distinct()
|
||||
.sorted()
|
||||
.toList();
|
||||
ctx.close();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
result = "FAILS to start";
|
||||
}
|
||||
outcome.put(value, result);
|
||||
t.line(String.format("%-16s -> %s", value, result));
|
||||
}
|
||||
t.write();
|
||||
if (Transcript.LABEL.equals("4.2.0-RC1")) {
|
||||
assertThat(outcome.get("open-telemetry")).isEqualTo("FAILS to start");
|
||||
assertThat(outcome.get("opentelemetry")).contains("http.server.request.duration");
|
||||
assertThat(outcome.get("micrometer")).contains("http.server.requests");
|
||||
}
|
||||
else if (!Transcript.is42()) {
|
||||
assertThat(outcome.values()).allMatch(v -> v.contains("http.server.requests"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.ankurm.otel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
/**
|
||||
* One request against /hello/{name}, measured with each value of
|
||||
* management.observations.conventions (and with the property absent). Writes the HTTP meter names
|
||||
* and tags, and the span-related strings found in the OTLP trace payload. A start-up failure is
|
||||
* recorded, not hidden.
|
||||
*/
|
||||
class ConventionsTest {
|
||||
|
||||
@Test
|
||||
void metricsAndSpans() throws Exception {
|
||||
Transcript t = new Transcript("01-conventions-metrics-and-spans");
|
||||
java.util.Map<String, List<String>> meters = new java.util.LinkedHashMap<>();
|
||||
java.util.Map<String, String> failures = new java.util.LinkedHashMap<>();
|
||||
for (String mode : List.of("(property not set)", "micrometer", "open-telemetry", "opentelemetry")) {
|
||||
try (OtlpSink sink = new OtlpSink()) {
|
||||
t.line("");
|
||||
t.line("== management.observations.conventions=" + mode);
|
||||
ConfigurableApplicationContext ctx = null;
|
||||
try {
|
||||
String[] props = mode.startsWith("(")
|
||||
? new String[] { "management.otlp.metrics.export.enabled=false",
|
||||
"management.opentelemetry.tracing.export.otlp.endpoint=" + sink.baseUrl() + "/v1/traces" }
|
||||
: new String[] { "management.observations.conventions=" + mode,
|
||||
"management.otlp.metrics.export.enabled=false",
|
||||
"management.opentelemetry.tracing.export.otlp.endpoint=" + sink.baseUrl() + "/v1/traces" };
|
||||
ctx = Apps.start(props);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Throwable root = ex;
|
||||
while (root.getCause() != null) {
|
||||
root = root.getCause();
|
||||
}
|
||||
t.line("-- application FAILED to start");
|
||||
t.line(" " + root.getClass().getSimpleName() + ": " + firstLine(root.getMessage()));
|
||||
failures.put(mode, root.getClass().getSimpleName());
|
||||
continue;
|
||||
}
|
||||
Apps.call(ctx, "/hello/ankur");
|
||||
MeterRegistry registry = ctx.getBean(MeterRegistry.class);
|
||||
Apps.awaitHttpMeter(registry);
|
||||
t.line("-- meters whose name contains 'http':");
|
||||
List<String> found = Apps.httpMeters(registry);
|
||||
meters.put(mode, found);
|
||||
found.forEach(l -> t.line(" " + l));
|
||||
ctx.close();
|
||||
Thread.sleep(500);
|
||||
t.line("-- span-related strings in the OTLP trace payload (" + sink.hits().size() + " request(s)):");
|
||||
sink.hits().stream()
|
||||
.flatMap(h -> h.strings().stream())
|
||||
.filter(s -> s.contains("hello") || s.contains("GET") || s.contains("get "))
|
||||
.distinct()
|
||||
.forEach(s -> t.line(" " + s));
|
||||
}
|
||||
}
|
||||
t.write();
|
||||
if (Transcript.is42()) {
|
||||
assertThat(meters.get("(property not set)")).anyMatch(l -> l.startsWith("http.server.requests "));
|
||||
assertThat(meters.get("micrometer")).anyMatch(l -> l.startsWith("http.server.requests "));
|
||||
assertThat(meters.get("opentelemetry")).anyMatch(l -> l.startsWith("http.server.request.duration "));
|
||||
assertThat(meters.get("opentelemetry")).noneMatch(l -> l.startsWith("http.server.requests "));
|
||||
}
|
||||
if (Transcript.LABEL.equals("4.2.0-RC1")) {
|
||||
// Pinned on purpose: when this stops failing, the RC1 section of the article is stale.
|
||||
assertThat(failures).containsKey("open-telemetry");
|
||||
assertThat(failures).doesNotContainKey("opentelemetry");
|
||||
}
|
||||
else if (!Transcript.is42()) {
|
||||
assertThat(failures).isEmpty();
|
||||
assertThat(meters.get("opentelemetry")).anyMatch(l -> l.startsWith("http.server.requests "));
|
||||
}
|
||||
}
|
||||
|
||||
private static String firstLine(String s) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
int nl = s.indexOf('\n');
|
||||
String line = nl < 0 ? s : s.substring(0, nl);
|
||||
return line.length() > 260 ? line.substring(0, 260) + "..." : line;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.ankurm.otel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Two smaller 4.2 changes that break dashboards and log queries rather than builds: the MDC keys
|
||||
* that carry the trace and span ids, and the active-observation LongTaskTimer that is no longer
|
||||
* created by default.
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class MdcAndLongTaskTimerTest {
|
||||
|
||||
@Test
|
||||
void mdcKeys(CapturedOutput output) {
|
||||
Transcript t = new Transcript("04-mdc-keys");
|
||||
// The pattern reads both the default key names and the custom ones, so one log line shows
|
||||
// which key the tracing bridge really populated.
|
||||
ConfigurableApplicationContext ctx = Apps.start("management.otlp.metrics.export.enabled=false",
|
||||
"management.tracing.mdc.trace-id-key=trace_id", "management.tracing.mdc.span-id-key=span_id",
|
||||
"logging.pattern.correlation=default[%X{traceId:-}] custom[%X{trace_id:-}] ");
|
||||
Apps.call(ctx, "/hello/ankur");
|
||||
ctx.close();
|
||||
String line = output.getAll().lines().filter(l -> l.contains("handling hello for ankur")).findFirst().orElseThrow();
|
||||
String correlation = line.substring(line.indexOf("default["), line.indexOf("] : ") > 0 ? line.indexOf("] :") : line.length());
|
||||
t.line("management.tracing.mdc.trace-id-key = trace_id");
|
||||
t.line("management.tracing.mdc.span-id-key = span_id");
|
||||
t.line("log pattern correlation part reads both MDC keys");
|
||||
t.line("");
|
||||
t.line("log line: " + line.replaceAll("^\\S+\\s+", "").replaceAll("(?<=\\[)[0-9a-f]{32}(?=\\])", "<32-hex-trace-id>"));
|
||||
boolean defaultKeyFilled = line.matches(".*default\\[[0-9a-f]{32}\\].*");
|
||||
boolean customKeyFilled = line.matches(".*custom\\[[0-9a-f]{32}\\].*");
|
||||
t.line("default key traceId populated: " + defaultKeyFilled);
|
||||
t.line("custom key trace_id populated: " + customKeyFilled);
|
||||
if (Transcript.is42()) {
|
||||
assertThat(customKeyFilled).isTrue();
|
||||
assertThat(defaultKeyFilled).isFalse();
|
||||
}
|
||||
else {
|
||||
assertThat(customKeyFilled).isFalse();
|
||||
assertThat(defaultKeyFilled).isTrue();
|
||||
}
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeLongTaskTimer() throws Exception {
|
||||
Transcript t = new Transcript("05-active-long-task-timer");
|
||||
for (String setting : List.of("(default)", "management.metrics.observations.include-active-long-task-timer=true")) {
|
||||
ConfigurableApplicationContext ctx = setting.startsWith("(")
|
||||
? Apps.start("management.otlp.metrics.export.enabled=false")
|
||||
: Apps.start("management.otlp.metrics.export.enabled=false", setting);
|
||||
Apps.call(ctx, "/hello/ankur");
|
||||
MeterRegistry registry = ctx.getBean(MeterRegistry.class);
|
||||
Apps.awaitHttpMeter(registry);
|
||||
TreeSet<String> names = new TreeSet<>();
|
||||
for (Meter m : registry.getMeters()) {
|
||||
if (m.getId().getName().startsWith("http.server")) {
|
||||
names.add(m.getId().getName() + " (" + m.getId().getType() + ")");
|
||||
}
|
||||
}
|
||||
t.line("");
|
||||
t.line("== " + setting);
|
||||
names.forEach(n -> t.line(" " + n));
|
||||
boolean active = names.stream().anyMatch(n -> n.startsWith("http.server.requests.active"));
|
||||
t.line(" http.server.requests.active present: " + active);
|
||||
if (Transcript.is42()) {
|
||||
assertThat(active).isEqualTo(!setting.startsWith("("));
|
||||
}
|
||||
else if (setting.startsWith("(")) {
|
||||
assertThat(active).isTrue();
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
t.write();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.ankurm.otel;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
/**
|
||||
* A tiny OTLP/HTTP receiver: it accepts any POST, answers 200 with an empty protobuf body and
|
||||
* remembers the path, a few headers and the (decompressed) payload bytes. Enough to see where an
|
||||
* exporter really sends data, with which headers, and which strings are inside the payload.
|
||||
*/
|
||||
final class OtlpSink implements AutoCloseable {
|
||||
|
||||
record Hit(String path, String authorization, String xShared, String contentEncoding, String contentType, byte[] body) {
|
||||
|
||||
/** Printable ASCII runs of at least 6 characters, a poor man's protobuf string reader. */
|
||||
List<String> strings() {
|
||||
TreeSet<String> out = new TreeSet<>();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : body) {
|
||||
if (b >= 32 && b < 127) {
|
||||
sb.append((char) b);
|
||||
}
|
||||
else {
|
||||
flush(sb, out);
|
||||
}
|
||||
}
|
||||
flush(sb, out);
|
||||
return new ArrayList<>(out);
|
||||
}
|
||||
|
||||
private static void flush(StringBuilder sb, TreeSet<String> out) {
|
||||
if (sb.length() >= 6) {
|
||||
out.add(sb.toString());
|
||||
}
|
||||
sb.setLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
private final HttpServer server;
|
||||
private final List<Hit> hits = new CopyOnWriteArrayList<>();
|
||||
|
||||
OtlpSink() throws IOException {
|
||||
server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
|
||||
server.createContext("/", exchange -> {
|
||||
byte[] raw = exchange.getRequestBody().readAllBytes();
|
||||
String enc = exchange.getRequestHeaders().getFirst("Content-Encoding");
|
||||
byte[] body = raw;
|
||||
if ("gzip".equalsIgnoreCase(enc)) {
|
||||
try (InputStream in = new GZIPInputStream(new java.io.ByteArrayInputStream(raw))) {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
in.transferTo(bos);
|
||||
body = bos.toByteArray();
|
||||
}
|
||||
}
|
||||
hits.add(new Hit(exchange.getRequestURI().getPath(), exchange.getRequestHeaders().getFirst("Authorization"),
|
||||
exchange.getRequestHeaders().getFirst("X-Shared"), enc, exchange.getRequestHeaders().getFirst("Content-Type"), body));
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/x-protobuf");
|
||||
exchange.sendResponseHeaders(200, -1);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
}
|
||||
|
||||
int port() {
|
||||
return server.getAddress().getPort();
|
||||
}
|
||||
|
||||
String baseUrl() {
|
||||
return "http://localhost:" + port();
|
||||
}
|
||||
|
||||
List<Hit> hits() {
|
||||
return List.copyOf(hits);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
server.stop(0);
|
||||
hits.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ankurm.otel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* What a dashboard query sees. Scrapes /actuator/prometheus after one request and lists the
|
||||
* http_server* families and one labelled sample, for the default conventions and for the
|
||||
* open-telemetry conventions (spelled the way RC1 accepts it).
|
||||
*/
|
||||
class PrometheusNamesTest {
|
||||
|
||||
@Test
|
||||
void promNames() throws Exception {
|
||||
Transcript t = new Transcript("06-prometheus-names");
|
||||
List<String> modes = List.of("micrometer", "opentelemetry");
|
||||
java.util.Map<String, List<String>> families = new java.util.LinkedHashMap<>();
|
||||
for (String mode : modes) {
|
||||
ConfigurableApplicationContext ctx = Apps.start("management.observations.conventions=" + mode,
|
||||
"management.otlp.metrics.export.enabled=false");
|
||||
Apps.call(ctx, "/hello/ankur");
|
||||
io.micrometer.core.instrument.MeterRegistry registry = ctx.getBean(io.micrometer.core.instrument.MeterRegistry.class);
|
||||
Apps.awaitHttpMeter(registry);
|
||||
String port = ctx.getEnvironment().getProperty("local.server.port");
|
||||
String body = RestClient.create().get().uri("http://localhost:" + port + "/actuator/prometheus")
|
||||
.retrieve().body(String.class);
|
||||
TreeSet<String> names = new TreeSet<>();
|
||||
String sample = "";
|
||||
for (String line : body.split("\n")) {
|
||||
if (line.startsWith("http_server")) {
|
||||
names.add(line.substring(0, line.indexOf('{') > 0 ? line.indexOf('{') : line.indexOf(' ')));
|
||||
if (line.contains("_count{") && sample.isEmpty()) {
|
||||
sample = line.substring(0, line.lastIndexOf(' '));
|
||||
}
|
||||
}
|
||||
}
|
||||
t.line("");
|
||||
t.line("== management.observations.conventions=" + mode);
|
||||
names.forEach(n -> t.line(" " + n));
|
||||
t.line(" sample: " + sample);
|
||||
families.put(mode, new ArrayList<>(names));
|
||||
ctx.close();
|
||||
}
|
||||
t.write();
|
||||
if (Transcript.is42()) {
|
||||
assertThat(families.get("micrometer")).contains("http_server_requests_seconds_count");
|
||||
assertThat(families.get("opentelemetry")).contains("http_server_request_duration_seconds_count");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ankurm.otel;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 4.2.0-M2 added management.opentelemetry.otlp.{endpoint,headers,compression}, shared by traces,
|
||||
* logs and metrics. A local OTLP receiver shows which paths the exporters really call, with which
|
||||
* headers, and what a signal-specific property does to the shared one.
|
||||
*/
|
||||
class SharedOtlpTest {
|
||||
|
||||
@Test
|
||||
void sharedEndpointHeadersAndCompression() throws Exception {
|
||||
Transcript t = new Transcript("02-shared-otlp-endpoint");
|
||||
try (OtlpSink sink = new OtlpSink()) {
|
||||
ConfigurableApplicationContext ctx = Apps.start(
|
||||
"management.opentelemetry.otlp.endpoint=" + sink.baseUrl(),
|
||||
"management.opentelemetry.otlp.headers.Authorization=Bearer shared-token",
|
||||
"management.opentelemetry.otlp.compression=gzip",
|
||||
"management.otlp.metrics.export.step=1s");
|
||||
Apps.call(ctx, "/hello/ankur");
|
||||
Thread.sleep(2500);
|
||||
ctx.close();
|
||||
Thread.sleep(500);
|
||||
t.line("management.opentelemetry.otlp.endpoint = <sink> (one property, no per-signal URLs)");
|
||||
t.line("management.opentelemetry.otlp.headers = Authorization: Bearer shared-token");
|
||||
t.line("management.opentelemetry.otlp.compression = gzip");
|
||||
t.line("");
|
||||
TreeSet<String> seen = new TreeSet<>();
|
||||
for (OtlpSink.Hit h : sink.hits()) {
|
||||
seen.add(String.format("POST %-12s Authorization=%s Content-Encoding=%s", h.path(), h.authorization(),
|
||||
h.contentEncoding()));
|
||||
}
|
||||
t.line("requests that reached the sink:");
|
||||
seen.forEach(l -> t.line(" " + l));
|
||||
if (Transcript.is42()) {
|
||||
assertThat(seen).anyMatch(l -> l.contains("/v1/traces") && l.contains("Bearer shared-token") && l.contains("gzip"));
|
||||
assertThat(seen).anyMatch(l -> l.contains("/v1/metrics") && l.contains("Bearer shared-token") && l.contains("gzip"));
|
||||
}
|
||||
else {
|
||||
assertThat(sink.hits()).isEmpty();
|
||||
t.line(" (none: 4.1.2 ignores the shared properties and exports to its default localhost:4318)");
|
||||
}
|
||||
}
|
||||
t.write();
|
||||
}
|
||||
|
||||
@Test
|
||||
void signalSpecificPropertyWins() throws Exception {
|
||||
Transcript t = new Transcript("03-signal-specific-overrides-shared");
|
||||
try (OtlpSink sink = new OtlpSink()) {
|
||||
ConfigurableApplicationContext ctx = Apps.start(
|
||||
"management.opentelemetry.otlp.endpoint=" + sink.baseUrl(),
|
||||
"management.opentelemetry.otlp.headers.Authorization=Bearer shared-token",
|
||||
"management.opentelemetry.otlp.headers.X-Shared=shared-only",
|
||||
"management.opentelemetry.tracing.export.otlp.endpoint=" + sink.baseUrl() + "/custom/spans",
|
||||
"management.opentelemetry.tracing.export.otlp.headers.Authorization=Bearer traces-token",
|
||||
"management.otlp.metrics.export.step=1s");
|
||||
Apps.call(ctx, "/hello/ankur");
|
||||
Thread.sleep(2500);
|
||||
ctx.close();
|
||||
Thread.sleep(500);
|
||||
t.line("shared endpoint = <sink>");
|
||||
t.line("tracing.export.otlp.endpoint = <sink>/custom/spans");
|
||||
t.line("shared header Authorization = Bearer shared-token");
|
||||
t.line("shared header X-Shared = shared-only");
|
||||
t.line("tracing.export.otlp.headers.Authorization = Bearer traces-token");
|
||||
t.line("");
|
||||
List<String> lines = sink.hits().stream()
|
||||
.map(h -> String.format("POST %-14s Authorization=%s X-Shared=%s", h.path(), h.authorization(), h.xShared()))
|
||||
.distinct()
|
||||
.sorted()
|
||||
.toList();
|
||||
t.line("requests that reached the sink:");
|
||||
lines.forEach(l -> t.line(" " + l));
|
||||
if (Transcript.is42()) {
|
||||
assertThat(lines).anyMatch(l -> l.contains("/custom/spans") && l.contains("Bearer traces-token")
|
||||
&& l.contains("X-Shared=shared-only"));
|
||||
assertThat(lines).anyMatch(l -> l.contains("/v1/metrics") && l.contains("Bearer shared-token")
|
||||
&& l.contains("X-Shared=shared-only"));
|
||||
assertThat(lines).noneMatch(l -> l.contains("/custom/spans/v1/traces"));
|
||||
}
|
||||
}
|
||||
t.write();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ankurm.otel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Collects lines and writes output/NN-name[-on-4.1.2].txt. Tests assert what they write. */
|
||||
final class Transcript {
|
||||
|
||||
static final String LABEL = System.getProperty("boot.label", "4.2.0-RC1");
|
||||
|
||||
static boolean is42() {
|
||||
return LABEL.startsWith("4.2");
|
||||
}
|
||||
|
||||
private final List<String> lines = new ArrayList<>();
|
||||
private final Path file;
|
||||
|
||||
Transcript(String name) {
|
||||
String suffix = is42() ? "" : "-on-" + LABEL;
|
||||
this.file = Path.of("output", name + suffix + ".txt");
|
||||
line("# " + name + " (Spring Boot " + LABEL + ", JDK " + Runtime.version().feature() + ")");
|
||||
}
|
||||
|
||||
Transcript line(String s) {
|
||||
lines.add(s);
|
||||
return this;
|
||||
}
|
||||
|
||||
void write() {
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.write(file, lines);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user