From 4b4d56e97c31e9af02facf373c34ad529f0084f1 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Tue, 23 Sep 2025 16:48:25 -0400 Subject: [PATCH 1/5] implement ability to add manual telemetry to metadata --- .../docs/InstrumentationAnalyzer.java | 36 +- .../docs/internal/EmittedMetrics.java | 51 +++ .../internal/InstrumentationMetadata.java | 60 +++- .../docs/internal/ManualTelemetryEntry.java | 161 +++++++++ .../docs/internal/TelemetryMerger.java | 205 +++++++++++ .../docs/parsers/MetricParser.java | 13 +- .../docs/ManualTelemetryTest.java | 124 +++++++ .../docs/TelemetryMergerTest.java | 277 +++++++++++++++ .../v4_3/AbstractApacheHttpClientTest.java | 328 ++++++++++++++++++ .../apachehttpclient/v4_3/HttpUriRequest.java | 24 ++ 10 files changed, 1263 insertions(+), 16 deletions(-) create mode 100644 instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/ManualTelemetryEntry.java create mode 100644 instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/TelemetryMerger.java create mode 100644 instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/ManualTelemetryTest.java create mode 100644 instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/TelemetryMergerTest.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.java diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzer.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzer.java index a84c88dd95e9..5e3d26f3380f 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzer.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/InstrumentationAnalyzer.java @@ -8,9 +8,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.exc.ValueInstantiationException; +import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics; +import io.opentelemetry.instrumentation.docs.internal.EmittedSpans; import io.opentelemetry.instrumentation.docs.internal.InstrumentationMetadata; import io.opentelemetry.instrumentation.docs.internal.InstrumentationModule; import io.opentelemetry.instrumentation.docs.internal.InstrumentationType; +import io.opentelemetry.instrumentation.docs.internal.TelemetryMerger; import io.opentelemetry.instrumentation.docs.parsers.GradleParser; import io.opentelemetry.instrumentation.docs.parsers.MetricParser; import io.opentelemetry.instrumentation.docs.parsers.ModuleParser; @@ -64,8 +67,9 @@ private void enrichModule(InstrumentationModule module) throws IOException { } module.setTargetVersions(getVersionInformation(module)); - module.setMetrics(MetricParser.getMetrics(module, fileManager)); - module.setSpans(SpanParser.getSpans(module, fileManager)); + + // Handle telemetry merging (manual + emitted) + setMergedTelemetry(module, metaData); } @Nullable @@ -88,4 +92,32 @@ private Map> getVersionInformation( List gradleFiles = fileManager.findBuildGradleFiles(module.getSrcPath()); return GradleParser.extractVersions(gradleFiles, module); } + + /** + * Sets merged telemetry data on the module, combining manual telemetry from metadata.yaml with + * emitted telemetry from .telemetry files. + */ + private void setMergedTelemetry( + InstrumentationModule module, @Nullable InstrumentationMetadata metadata) throws IOException { + Map> emittedMetrics = + MetricParser.getMetrics(module, fileManager); + Map> emittedSpans = SpanParser.getSpans(module, fileManager); + + if (metadata != null && !metadata.getAdditionalTelemetry().isEmpty()) { + TelemetryMerger.MergedTelemetryData merged = + TelemetryMerger.merge( + metadata.getAdditionalTelemetry(), + metadata.getOverrideTelemetry(), + emittedMetrics, + emittedSpans, + module.getInstrumentationName()); + + module.setMetrics(merged.metrics()); + module.setSpans(merged.spans()); + } else { + // No manual telemetry, use emitted only + module.setMetrics(emittedMetrics); + module.setSpans(emittedSpans); + } + } } diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/EmittedMetrics.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/EmittedMetrics.java index c00f64898f0c..ff954f329e39 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/EmittedMetrics.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/EmittedMetrics.java @@ -8,6 +8,7 @@ import static java.util.Collections.emptyList; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.ArrayList; import java.util.List; @@ -157,5 +158,55 @@ public List getAttributes() { public void setAttributes(List attributes) { this.attributes = attributes; } + + /** + * Builder for creating EmittedMetrics.Metric instances. This class is internal and is hence not + * for public use. Its APIs are unstable and can change at any time. + */ + public static class Builder { + private String name = ""; + private String description = ""; + private String type = ""; + private String unit = ""; + private List attributes = new ArrayList<>(); + + @CanIgnoreReturnValue + public Builder name(String name) { + this.name = name; + return this; + } + + @CanIgnoreReturnValue + public Builder description(String description) { + this.description = description; + return this; + } + + @CanIgnoreReturnValue + public Builder type(String type) { + this.type = type; + return this; + } + + @CanIgnoreReturnValue + public Builder unit(String unit) { + this.unit = unit; + return this; + } + + @CanIgnoreReturnValue + public Builder attributes(List attributes) { + this.attributes = attributes != null ? attributes : new ArrayList<>(); + return this; + } + + public Metric build() { + return new Metric(name, description, type, unit, attributes); + } + } + + public static Builder builder() { + return new Builder(); + } } } diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/InstrumentationMetadata.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/InstrumentationMetadata.java index 2c7158b7b1c8..2a3f0ae60af7 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/InstrumentationMetadata.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/InstrumentationMetadata.java @@ -40,6 +40,13 @@ public class InstrumentationMetadata { @JsonProperty("semantic_conventions") private List semanticConventions = emptyList(); + @JsonProperty("additional_telemetry") + private List additionalTelemetry = emptyList(); + + @JsonProperty("override_telemetry") + @Nullable + private Boolean overrideTelemetry; + public InstrumentationMetadata() { this.classification = InstrumentationClassification.LIBRARY.name(); } @@ -59,6 +66,7 @@ public InstrumentationMetadata( this.displayName = displayName; this.semanticConventions = Objects.requireNonNullElse(semanticConventions, emptyList()); this.configurations = Objects.requireNonNullElse(configurations, emptyList()); + this.overrideTelemetry = null; } @Nullable @@ -123,6 +131,22 @@ public void setLibraryLink(@Nullable String libraryLink) { this.libraryLink = libraryLink; } + public List getAdditionalTelemetry() { + return additionalTelemetry; + } + + public void setAdditionalTelemetry(@Nullable List additionalTelemetry) { + this.additionalTelemetry = Objects.requireNonNullElse(additionalTelemetry, emptyList()); + } + + public Boolean getOverrideTelemetry() { + return Objects.requireNonNullElse(overrideTelemetry, false); + } + + public void setOverrideTelemetry(@Nullable Boolean overrideTelemetry) { + this.overrideTelemetry = overrideTelemetry; + } + /** * This class is internal and is hence not for public use. Its APIs are unstable and can change at * any time. @@ -136,6 +160,8 @@ public static class Builder { @Nullable private String displayName; private List configurations = emptyList(); private List semanticConventions = emptyList(); + private List additionalTelemetry = emptyList(); + @Nullable private Boolean overrideTelemetry; @CanIgnoreReturnValue public Builder description(@Nullable String description) { @@ -179,15 +205,33 @@ public Builder semanticConventions(@Nullable List semanticCo return this; } + @CanIgnoreReturnValue + public Builder additionalTelemetry(@Nullable List additionalTelemetry) { + this.additionalTelemetry = Objects.requireNonNullElse(additionalTelemetry, emptyList()); + return this; + } + + @CanIgnoreReturnValue + public Builder overrideTelemetry(@Nullable Boolean overrideTelemetry) { + this.overrideTelemetry = overrideTelemetry; + return this; + } + public InstrumentationMetadata build() { - return new InstrumentationMetadata( - description, - disabledByDefault, - classification != null ? classification : InstrumentationClassification.LIBRARY.name(), - libraryLink, - displayName, - semanticConventions, - configurations); + InstrumentationMetadata metadata = + new InstrumentationMetadata( + description, + disabledByDefault, + classification != null + ? classification + : InstrumentationClassification.LIBRARY.name(), + libraryLink, + displayName, + semanticConventions, + configurations); + metadata.setAdditionalTelemetry(additionalTelemetry); + metadata.setOverrideTelemetry(overrideTelemetry); + return metadata; } } } diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/ManualTelemetryEntry.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/ManualTelemetryEntry.java new file mode 100644 index 000000000000..4c836190d333 --- /dev/null +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/ManualTelemetryEntry.java @@ -0,0 +1,161 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.internal; + +import static java.util.Collections.emptyList; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * Represents a manual telemetry entry that can be specified directly in metadata.yaml files. This + * allows instrumentations to document their telemetry without relying solely on test-based + * collection. This class is internal and is hence not for public use. Its APIs are unstable and can + * change at any time. + */ +public class ManualTelemetryEntry { + private String when = "default"; + private List metrics = emptyList(); + private List spans = emptyList(); + + public ManualTelemetryEntry() {} + + public ManualTelemetryEntry( + String when, @Nullable List metrics, @Nullable List spans) { + this.when = when; + this.metrics = Objects.requireNonNullElse(metrics, emptyList()); + this.spans = Objects.requireNonNullElse(spans, emptyList()); + } + + public String getWhen() { + return when; + } + + public void setWhen(String when) { + this.when = when; + } + + public List getMetrics() { + return metrics; + } + + public void setMetrics(@Nullable List metrics) { + this.metrics = Objects.requireNonNullElse(metrics, emptyList()); + } + + public List getSpans() { + return spans; + } + + public void setSpans(@Nullable List spans) { + this.spans = Objects.requireNonNullElse(spans, emptyList()); + } + + /** + * Represents a manually specified metric. This class is internal and is hence not for public use. + * Its APIs are unstable and can change at any time. + */ + public static class ManualMetric { + private String name = ""; + private String description = ""; + private String type = ""; + private String unit = ""; + private List attributes = emptyList(); + + public ManualMetric() {} + + public ManualMetric( + String name, + String description, + String type, + String unit, + @Nullable List attributes) { + this.name = name; + this.description = description; + this.type = type; + this.unit = unit; + this.attributes = Objects.requireNonNullElse(attributes, emptyList()); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public List getAttributes() { + return attributes; + } + + public void setAttributes(@Nullable List attributes) { + this.attributes = Objects.requireNonNullElse(attributes, emptyList()); + } + } + + /** + * Represents a manually specified span. This class is internal and is hence not for public use. + * Its APIs are unstable and can change at any time. + */ + public static class ManualSpan { + @JsonProperty("span_kind") + private String spanKind = ""; + + private List attributes = emptyList(); + + public ManualSpan() {} + + public ManualSpan(String spanKind, @Nullable List attributes) { + this.spanKind = spanKind; + this.attributes = Objects.requireNonNullElse(attributes, emptyList()); + } + + @JsonProperty("span_kind") + public String getSpanKind() { + return spanKind; + } + + @JsonProperty("span_kind") + public void setSpanKind(String spanKind) { + this.spanKind = spanKind; + } + + public List getAttributes() { + return attributes; + } + + public void setAttributes(@Nullable List attributes) { + this.attributes = Objects.requireNonNullElse(attributes, emptyList()); + } + } +} diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/TelemetryMerger.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/TelemetryMerger.java new file mode 100644 index 000000000000..cd977513c900 --- /dev/null +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/TelemetryMerger.java @@ -0,0 +1,205 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +/** + * Handles merging of manual telemetry entries (from metadata.yaml) with emitted telemetry data + * (from .telemetry files). This class is internal and is hence not for public use. Its APIs are + * unstable and can change at any time. + */ +public class TelemetryMerger { + private static final Logger logger = Logger.getLogger(TelemetryMerger.class.getName()); + + /** + * Merges manual telemetry entries with emitted telemetry data based on the override setting. + * + * @param manualTelemetry the manual telemetry entries from metadata.yaml + * @param overrideTelemetry whether to override emitted telemetry + * @param emittedMetrics the emitted metrics from .telemetry files + * @param emittedSpans the emitted spans from .telemetry files + * @param instrumentationName the name of the instrumentation (for logging) + * @return merged telemetry data as separate maps for metrics and spans + */ + public static MergedTelemetryData merge( + List manualTelemetry, + boolean overrideTelemetry, + Map> emittedMetrics, + Map> emittedSpans, + String instrumentationName) { + + if (overrideTelemetry) { + logger.info( + "Override mode enabled for " + instrumentationName + ", ignoring emitted telemetry"); + return convertManualTelemetryOnly(manualTelemetry); + } + + return mergeManualAndAutoGenerated( + manualTelemetry, emittedMetrics, emittedSpans, instrumentationName); + } + + /** Converts manual telemetry entries to the standard format, ignoring emitted data. */ + private static MergedTelemetryData convertManualTelemetryOnly( + List manualTelemetry) { + Map> metrics = new HashMap<>(); + Map> spans = new HashMap<>(); + + for (ManualTelemetryEntry entry : manualTelemetry) { + String when = entry.getWhen(); + + if (!entry.getMetrics().isEmpty()) { + List convertedMetrics = convertManualMetrics(entry.getMetrics()); + metrics.computeIfAbsent(when, k -> new ArrayList<>()).addAll(convertedMetrics); + } + + if (!entry.getSpans().isEmpty()) { + List convertedSpans = convertManualSpans(entry.getSpans()); + spans.computeIfAbsent(when, k -> new ArrayList<>()).addAll(convertedSpans); + } + } + + return new MergedTelemetryData(metrics, spans); + } + + /** + * Merges manual telemetry with emitted telemetry, deduplicating by name within the same 'when' + * condition. + */ + private static MergedTelemetryData mergeManualAndAutoGenerated( + List manualTelemetry, + Map> emittedMetrics, + Map> emittedSpans, + String instrumentationName) { + + // Start with generated data (create mutable copies of the lists) + Map> mergedMetrics = new HashMap<>(); + for (Map.Entry> entry : emittedMetrics.entrySet()) { + mergedMetrics.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + + Map> mergedSpans = new HashMap<>(); + for (Map.Entry> entry : emittedSpans.entrySet()) { + mergedSpans.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + + // Add manual telemetry, deduplicating by name + for (ManualTelemetryEntry entry : manualTelemetry) { + String when = entry.getWhen(); + + if (!entry.getMetrics().isEmpty()) { + List convertedMetrics = convertManualMetrics(entry.getMetrics()); + List existingMetrics = + mergedMetrics.computeIfAbsent(when, k -> new ArrayList<>()); + mergeMetricsList(existingMetrics, convertedMetrics, when, instrumentationName); + } + + if (!entry.getSpans().isEmpty()) { + List convertedSpans = convertManualSpans(entry.getSpans()); + List existingSpans = + mergedSpans.computeIfAbsent(when, k -> new ArrayList<>()); + mergeSpansList(existingSpans, convertedSpans, when, instrumentationName); + } + } + + return new MergedTelemetryData(mergedMetrics, mergedSpans); + } + + /** Merges metrics lists, deduplicating by metric name and logging conflicts. */ + private static void mergeMetricsList( + List existing, + List toAdd, + String when, + String instrumentationName) { + + Set existingNames = new HashSet<>(); + for (EmittedMetrics.Metric metric : existing) { + existingNames.add(metric.getName()); + } + + for (EmittedMetrics.Metric metric : toAdd) { + if (existingNames.contains(metric.getName())) { + logger.warning( + String.format( + "Manual metric '%s' in 'when: %s' conflicts with emitted metric in %s. Using manual definition.", + metric.getName(), when, instrumentationName)); + // Remove the existing metric and add the manual one + existing.removeIf(m -> m.getName().equals(metric.getName())); + } + existing.add(metric); + existingNames.add(metric.getName()); + } + } + + /** Merges spans lists, deduplicating by span kind and logging conflicts. */ + private static void mergeSpansList( + List existing, + List toAdd, + String when, + String instrumentationName) { + + Set existingKinds = new HashSet<>(); + for (EmittedSpans.Span span : existing) { + existingKinds.add(span.getSpanKind()); + } + + for (EmittedSpans.Span span : toAdd) { + if (existingKinds.contains(span.getSpanKind())) { + logger.warning( + String.format( + "Manual span kind '%s' in 'when: %s' conflicts with emitted span in %s. Using manual definition.", + span.getSpanKind(), when, instrumentationName)); + // Remove the existing span and add the manual one + existing.removeIf(s -> s.getSpanKind().equals(span.getSpanKind())); + } + existing.add(span); + existingKinds.add(span.getSpanKind()); + } + } + + /** Converts manual metrics to the standard EmittedMetrics.Metric format. */ + private static List convertManualMetrics( + List manualMetrics) { + List converted = new ArrayList<>(); + for (ManualTelemetryEntry.ManualMetric manual : manualMetrics) { + converted.add( + EmittedMetrics.Metric.builder() + .name(manual.getName()) + .description(manual.getDescription()) + .type(manual.getType()) + .unit(manual.getUnit()) + .attributes(manual.getAttributes()) + .build()); + } + return converted; + } + + /** Converts manual spans to the standard EmittedSpans.Span format. */ + private static List convertManualSpans( + List manualSpans) { + List converted = new ArrayList<>(); + for (ManualTelemetryEntry.ManualSpan manual : manualSpans) { + converted.add(new EmittedSpans.Span(manual.getSpanKind(), manual.getAttributes())); + } + return converted; + } + + /** + * Container for merged telemetry data. This class is internal and is hence not for public use. + * Its APIs are unstable and can change at any time. + */ + public record MergedTelemetryData( + Map> metrics, + Map> spans) {} + + private TelemetryMerger() {} +} diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/MetricParser.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/MetricParser.java index e167de0dd458..9f2fed43a5b9 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/MetricParser.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/parsers/MetricParser.java @@ -146,12 +146,13 @@ public static Map> buildFilteredMetrics( List metrics = result.computeIfAbsent(when, k -> new ArrayList<>()); for (AggregatedMetricInfo aggInfo : entry.getValue().values()) { metrics.add( - new EmittedMetrics.Metric( - aggInfo.name, - aggInfo.description, - aggInfo.type, - aggInfo.unit, - new ArrayList<>(aggInfo.attributes))); + EmittedMetrics.Metric.builder() + .name(aggInfo.name) + .description(aggInfo.description) + .type(aggInfo.type) + .unit(aggInfo.unit) + .attributes(new ArrayList<>(aggInfo.attributes)) + .build()); } } return result; diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/ManualTelemetryTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/ManualTelemetryTest.java new file mode 100644 index 000000000000..7a60bc9cf201 --- /dev/null +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/ManualTelemetryTest.java @@ -0,0 +1,124 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.opentelemetry.instrumentation.docs.internal.InstrumentationMetadata; +import io.opentelemetry.instrumentation.docs.internal.ManualTelemetryEntry; +import io.opentelemetry.instrumentation.docs.utils.YamlHelper; +import org.junit.jupiter.api.Test; + +class ManualTelemetryTest { + + @Test + void testManualTelemetryParsing() throws JsonProcessingException { + String yamlContent = + """ + description: "Example instrumentation with manual telemetry documentation" + semantic_conventions: + - HTTP_CLIENT_SPANS + library_link: https://example.com/library + additional_telemetry: + - when: default + metrics: + - name: system.disk.io + description: System disk IO + type: LONG_SUM + unit: By + attributes: + - name: device + type: STRING + - name: direction + type: STRING + spans: + - span_kind: CLIENT + attributes: + - name: custom.operation + type: STRING + - when: experimental + metrics: + - name: experimental.feature.usage + description: Usage of experimental features + type: HISTOGRAM + unit: s + attributes: + - name: feature.name + type: STRING + """; + + InstrumentationMetadata metadata = YamlHelper.metaDataParser(yamlContent); + + assertThat(metadata).isNotNull(); + assertThat(metadata.getDescription()) + .isEqualTo("Example instrumentation with manual telemetry documentation"); + assertThat(metadata.getLibraryLink()).isEqualTo("https://example.com/library"); + assertThat(metadata.getOverrideTelemetry()).isFalse(); + + assertThat(metadata.getAdditionalTelemetry()).hasSize(2); + + ManualTelemetryEntry defaultEntry = metadata.getAdditionalTelemetry().get(0); + assertThat(defaultEntry.getWhen()).isEqualTo("default"); + assertThat(defaultEntry.getMetrics()).hasSize(1); + assertThat(defaultEntry.getSpans()).hasSize(1); + + ManualTelemetryEntry.ManualMetric metric = defaultEntry.getMetrics().get(0); + assertThat(metric.getName()).isEqualTo("system.disk.io"); + assertThat(metric.getDescription()).isEqualTo("System disk IO"); + assertThat(metric.getType()).isEqualTo("LONG_SUM"); + assertThat(metric.getUnit()).isEqualTo("By"); + assertThat(metric.getAttributes()).hasSize(2); + + ManualTelemetryEntry.ManualSpan span = defaultEntry.getSpans().get(0); + assertThat(span.getSpanKind()).isEqualTo("CLIENT"); + assertThat(span.getAttributes()).hasSize(1); + + ManualTelemetryEntry experimentalEntry = metadata.getAdditionalTelemetry().get(1); + assertThat(experimentalEntry.getWhen()).isEqualTo("experimental"); + assertThat(experimentalEntry.getMetrics()).hasSize(1); + assertThat(experimentalEntry.getSpans()).isEmpty(); + } + + @Test + void testOverrideTelemetryFlag() throws JsonProcessingException { + String yamlContent = + """ + description: "Example with override" + override_telemetry: true + additional_telemetry: + - when: default + metrics: + - name: manual.metric + description: Manual metric only + type: COUNTER + unit: "1" + attributes: [] + """; + + InstrumentationMetadata metadata = YamlHelper.metaDataParser(yamlContent); + + assertThat(metadata).isNotNull(); + assertThat(metadata.getOverrideTelemetry()).isTrue(); + assertThat(metadata.getAdditionalTelemetry()).hasSize(1); + } + + @Test + void testEmptyAdditionalTelemetry() throws JsonProcessingException { + String yamlContent = + """ + description: "Example without manual telemetry" + semantic_conventions: + - HTTP_CLIENT_SPANS + """; + + InstrumentationMetadata metadata = YamlHelper.metaDataParser(yamlContent); + + assertThat(metadata).isNotNull(); + assertThat(metadata.getOverrideTelemetry()).isFalse(); + assertThat(metadata.getAdditionalTelemetry()).isEmpty(); + } +} diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/TelemetryMergerTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/TelemetryMergerTest.java new file mode 100644 index 000000000000..169089dbaf73 --- /dev/null +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/TelemetryMergerTest.java @@ -0,0 +1,277 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.docs; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.docs.internal.EmittedMetrics; +import io.opentelemetry.instrumentation.docs.internal.EmittedSpans; +import io.opentelemetry.instrumentation.docs.internal.ManualTelemetryEntry; +import io.opentelemetry.instrumentation.docs.internal.TelemetryAttribute; +import io.opentelemetry.instrumentation.docs.internal.TelemetryMerger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class TelemetryMergerTest { + + @Test + void testOverrideMode() { + List manualTelemetry = createManualTelemetry(); + + // Emitted telemetry that should be ignored + Map> autoMetrics = createAutoGeneratedMetrics(); + Map> autoSpans = createAutoGeneratedSpans(); + + TelemetryMerger.MergedTelemetryData result = + TelemetryMerger.merge( + manualTelemetry, + true, // override mode + autoMetrics, + autoSpans, + "test-instrumentation"); + + // Should only contain manual telemetry + assertThat(result.metrics()).hasSize(1); + assertThat(result.spans()).hasSize(1); + + List defaultMetrics = result.metrics().get("default"); + assertThat(defaultMetrics).hasSize(1); + assertThat(defaultMetrics.get(0).getName()).isEqualTo("manual.metric"); + + List defaultSpans = result.spans().get("default"); + assertThat(defaultSpans).hasSize(1); + assertThat(defaultSpans.get(0).getSpanKind()).isEqualTo("CLIENT"); + } + + @Test + void testMergeMode() { + List manualTelemetry = createManualTelemetry(); + + Map> autoMetrics = createAutoGeneratedMetrics(); + Map> autoSpans = createAutoGeneratedSpans(); + + TelemetryMerger.MergedTelemetryData result = + TelemetryMerger.merge( + manualTelemetry, + false, // merge mode + autoMetrics, + autoSpans, + "test-instrumentation"); + + // Should contain both manual and emitted telemetry + assertThat(result.metrics()).hasSize(1); + assertThat(result.spans()).hasSize(1); + + List defaultMetrics = result.metrics().get("default"); + assertThat(defaultMetrics).hasSize(2); + + List defaultSpans = result.spans().get("default"); + assertThat(defaultSpans).hasSize(2); + } + + @Test + void testConflictResolution() { + // Create manual telemetry with same name as emitted + List manualTelemetry = createConflictingManualTelemetry(); + + Map> autoMetrics = createAutoGeneratedMetrics(); + Map> autoSpans = createAutoGeneratedSpans(); + + // Test merge mode with conflicts + TelemetryMerger.MergedTelemetryData result = + TelemetryMerger.merge( + manualTelemetry, + false, // merge mode + autoMetrics, + autoSpans, + "test-instrumentation"); + + // Should prefer manual telemetry for conflicts + List defaultMetrics = result.metrics().get("default"); + assertThat(defaultMetrics).hasSize(1); + EmittedMetrics.Metric metric = defaultMetrics.get(0); + assertThat(metric.getName()).isEqualTo("auto.metric"); + assertThat(metric.getDescription()).isEqualTo("Manual description overrides auto"); + + List defaultSpans = result.spans().get("default"); + assertThat(defaultSpans).hasSize(1); + EmittedSpans.Span span = defaultSpans.get(0); + assertThat(span.getSpanKind()).isEqualTo("SERVER"); + assertThat(span.getAttributes()).hasSize(1); + assertThat(span.getAttributes().get(0).getName()).isEqualTo("manual.attribute"); + } + + @Test + void testEmptyManualTelemetry() { + // Empty manual telemetry + List manualTelemetry = new ArrayList<>(); + + Map> autoMetrics = createAutoGeneratedMetrics(); + Map> autoSpans = createAutoGeneratedSpans(); + + // Test merge mode with empty manual telemetry + TelemetryMerger.MergedTelemetryData result = + TelemetryMerger.merge( + manualTelemetry, + false, // merge mode + autoMetrics, + autoSpans, + "test-instrumentation"); + + // Should only contain emitted telemetry + assertThat(result.metrics()).hasSize(1); + assertThat(result.spans()).hasSize(1); + + List defaultMetrics = result.metrics().get("default"); + assertThat(defaultMetrics).hasSize(1); + assertThat(defaultMetrics.get(0).getName()).isEqualTo("auto.metric"); + + List defaultSpans = result.spans().get("default"); + assertThat(defaultSpans).hasSize(1); + assertThat(defaultSpans.get(0).getSpanKind()).isEqualTo("SERVER"); + } + + @Test + void testMultipleWhenConditions() { + List manualTelemetry = createMultipleWhenManualTelemetry(); + + // Test override mode with multiple conditions + TelemetryMerger.MergedTelemetryData result = + TelemetryMerger.merge( + manualTelemetry, + true, // override mode + new HashMap<>(), + new HashMap<>(), + "test-instrumentation"); + + assertThat(result.metrics()).hasSize(2); + assertThat(result.metrics()).containsKey("default"); + assertThat(result.metrics()).containsKey("experimental"); + + assertThat(result.spans()).hasSize(1); + assertThat(result.spans()).containsKey("default"); + } + + private static List createManualTelemetry() { + List manualTelemetry = new ArrayList<>(); + + ManualTelemetryEntry entry = new ManualTelemetryEntry(); + entry.setWhen("default"); + + ManualTelemetryEntry.ManualMetric metric = new ManualTelemetryEntry.ManualMetric(); + metric.setName("manual.metric"); + metric.setDescription("Manual metric description"); + metric.setType("COUNTER"); + metric.setUnit("1"); + List metricAttrs = new ArrayList<>(); + metricAttrs.add(new TelemetryAttribute("manual.attr", "STRING")); + metric.setAttributes(metricAttrs); + entry.setMetrics(List.of(metric)); + + ManualTelemetryEntry.ManualSpan span = new ManualTelemetryEntry.ManualSpan(); + span.setSpanKind("CLIENT"); + List spanAttrs = new ArrayList<>(); + spanAttrs.add(new TelemetryAttribute("manual.span.attr", "STRING")); + span.setAttributes(spanAttrs); + entry.setSpans(List.of(span)); + + manualTelemetry.add(entry); + return manualTelemetry; + } + + private static List createConflictingManualTelemetry() { + List manualTelemetry = new ArrayList<>(); + + ManualTelemetryEntry entry = new ManualTelemetryEntry(); + entry.setWhen("default"); + + // Add manual metric with same name as emitted + ManualTelemetryEntry.ManualMetric metric = new ManualTelemetryEntry.ManualMetric(); + metric.setName("auto.metric"); + metric.setDescription("Manual description overrides auto"); + metric.setType("HISTOGRAM"); + metric.setUnit("s"); + entry.setMetrics(List.of(metric)); + + // Add manual span with same kind as emitted + ManualTelemetryEntry.ManualSpan span = new ManualTelemetryEntry.ManualSpan(); + span.setSpanKind("SERVER"); + List spanAttrs = new ArrayList<>(); + spanAttrs.add(new TelemetryAttribute("manual.attribute", "STRING")); + span.setAttributes(spanAttrs); + entry.setSpans(List.of(span)); + + manualTelemetry.add(entry); + return manualTelemetry; + } + + private static List createMultipleWhenManualTelemetry() { + List manualTelemetry = new ArrayList<>(); + + // Default condition + ManualTelemetryEntry defaultEntry = new ManualTelemetryEntry(); + defaultEntry.setWhen("default"); + ManualTelemetryEntry.ManualMetric defaultMetric = new ManualTelemetryEntry.ManualMetric(); + defaultMetric.setName("default.metric"); + defaultMetric.setDescription("Default metric"); + defaultMetric.setType("COUNTER"); + defaultMetric.setUnit("1"); + defaultEntry.setMetrics(List.of(defaultMetric)); + + ManualTelemetryEntry.ManualSpan defaultSpan = new ManualTelemetryEntry.ManualSpan(); + defaultSpan.setSpanKind("CLIENT"); + defaultEntry.setSpans(List.of(defaultSpan)); + + // Experimental condition + ManualTelemetryEntry experimentalEntry = new ManualTelemetryEntry(); + experimentalEntry.setWhen("experimental"); + ManualTelemetryEntry.ManualMetric expMetric = new ManualTelemetryEntry.ManualMetric(); + expMetric.setName("experimental.metric"); + expMetric.setDescription("Experimental metric"); + expMetric.setType("HISTOGRAM"); + expMetric.setUnit("s"); + experimentalEntry.setMetrics(List.of(expMetric)); + + manualTelemetry.add(defaultEntry); + manualTelemetry.add(experimentalEntry); + return manualTelemetry; + } + + private static Map> createAutoGeneratedMetrics() { + Map> autoMetrics = new HashMap<>(); + + List attrs = new ArrayList<>(); + attrs.add(new TelemetryAttribute("auto.attr", "STRING")); + + EmittedMetrics.Metric metric = + EmittedMetrics.Metric.builder() + .name("auto.metric") + .description("emitted metric") + .type("COUNTER") + .unit("1") + .attributes(attrs) + .build(); + + autoMetrics.put("default", new ArrayList<>(List.of(metric))); + return autoMetrics; + } + + private static Map> createAutoGeneratedSpans() { + Map> autoSpans = new HashMap<>(); + + EmittedSpans.Span span = new EmittedSpans.Span(); + span.setSpanKind("SERVER"); + List attrs = new ArrayList<>(); + attrs.add(new TelemetryAttribute("auto.span.attr", "STRING")); + span.setAttributes(attrs); + + autoSpans.put("default", new ArrayList<>(List.of(span))); + return autoSpans; + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java new file mode 100644 index 000000000000..5d7d198d7193 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java @@ -0,0 +1,328 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.util.Map; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.client.ResponseHandler; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicHttpRequest; +import org.apache.http.protocol.BasicHttpContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractApacheHttpClientTest { + + protected abstract InstrumentationExtension testing(); + + protected abstract CloseableHttpClient createClient(boolean readTimeout); + + private CloseableHttpClient client; + private CloseableHttpClient clientWithReadTimeout; + + @BeforeAll + void setUp() { + client = createClient(false); + clientWithReadTimeout = createClient(true); + } + + @AfterAll + void tearDown() throws Exception { + client.close(); + clientWithReadTimeout.close(); + } + + CloseableHttpClient getClient(URI uri) { + if (uri.toString().contains("/read-timeout")) { + return clientWithReadTimeout; + } + return client; + } + + abstract static class ApacheHttpClientTest extends AbstractHttpClientTest { + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.markAsLowLevelInstrumentation(); + } + } + + @Nested + class ApacheClientHostRequestTest extends ApacheHttpClientTest { + + @Override + public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { + // also testing with an absolute path below + return configureRequest(new BasicHttpRequest(method, fullPathFromUri(uri)), headers); + } + + @Override + public int sendRequest( + BasicHttpRequest request, String method, URI uri, Map headers) + throws Exception { + return getResponseCode( + getClient(uri) + .execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request)); + } + + @Override + public void sendRequestWithCallback( + BasicHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri) + .execute( + new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), + request, + responseCallback(httpClientResult)); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientHostRequestContextTest extends ApacheHttpClientTest { + + @Override + public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { + // also testing with an absolute path below + return configureRequest(new BasicHttpRequest(method, fullPathFromUri(uri)), headers); + } + + @Override + public int sendRequest( + BasicHttpRequest request, String method, URI uri, Map headers) + throws Exception { + return getResponseCode( + getClient(uri) + .execute( + new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), + request, + new BasicHttpContext())); + } + + @Override + public void sendRequestWithCallback( + BasicHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri) + .execute( + new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), + request, + responseCallback(httpClientResult), + new BasicHttpContext()); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientHostAbsoluteUriRequestTest extends ApacheHttpClientTest { + + @Override + public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { + return configureRequest(new BasicHttpRequest(method, uri.toString()), headers); + } + + @Override + public int sendRequest( + BasicHttpRequest request, String method, URI uri, Map headers) + throws Exception { + return getResponseCode( + getClient(uri) + .execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request)); + } + + @Override + public void sendRequestWithCallback( + BasicHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri) + .execute( + new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), + request, + responseCallback(httpClientResult)); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientHostAbsoluteUriRequestContextTest + extends ApacheHttpClientTest { + + @Override + public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { + return configureRequest(new BasicHttpRequest(method, uri.toString()), headers); + } + + @Override + public int sendRequest( + BasicHttpRequest request, String method, URI uri, Map headers) + throws Exception { + return getResponseCode( + getClient(uri) + .execute( + new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), + request, + new BasicHttpContext())); + } + + @Override + public void sendRequestWithCallback( + BasicHttpRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri) + .execute( + new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), + request, + responseCallback(httpClientResult), + new BasicHttpContext()); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientUriRequestTest extends ApacheHttpClientTest { + + @Override + public HttpUriRequest buildRequest(String method, URI uri, Map headers) { + // also testing with an absolute path below + return configureRequest(new HttpUriRequest(method, uri), headers); + } + + @Override + public int sendRequest( + HttpUriRequest request, String method, URI uri, Map headers) + throws Exception { + return getResponseCode(getClient(uri).execute(request)); + } + + @Override + public void sendRequestWithCallback( + HttpUriRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri).execute(request, responseCallback(httpClientResult)); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + @Nested + class ApacheClientUriRequestContextTest extends ApacheHttpClientTest { + + @Override + public HttpUriRequest buildRequest(String method, URI uri, Map headers) { + // also testing with an absolute path below + return configureRequest(new HttpUriRequest(method, uri), headers); + } + + @Override + public int sendRequest( + HttpUriRequest request, String method, URI uri, Map headers) + throws Exception { + return getResponseCode(getClient(uri).execute(request, new BasicHttpContext())); + } + + @Override + public void sendRequestWithCallback( + HttpUriRequest request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + try { + getClient(uri).execute(request, responseCallback(httpClientResult), new BasicHttpContext()); + } catch (Throwable t) { + httpClientResult.complete(t); + } + } + } + + static T configureRequest(T request, Map headers) { + request.addHeader("user-agent", "apachehttpclient"); + headers.forEach((key, value) -> request.setHeader(new BasicHeader(key, value))); + return request; + } + + static int getResponseCode(HttpResponse response) { + try { + if (response.getEntity() != null && response.getEntity().getContent() != null) { + response.getEntity().getContent().close(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return response.getStatusLine().getStatusCode(); + } + + static ResponseHandler responseCallback(HttpClientResult httpClientResult) { + return response -> { + try { + httpClientResult.complete(getResponseCode(response)); + } catch (Throwable t) { + httpClientResult.complete(t); + return response; + } + return response; + }; + } + + static String fullPathFromUri(URI uri) { + StringBuilder builder = new StringBuilder(); + if (uri.getPath() != null) { + builder.append(uri.getPath()); + } + + if (uri.getQuery() != null) { + builder.append('?'); + builder.append(uri.getQuery()); + } + + if (uri.getFragment() != null) { + builder.append('#'); + builder.append(uri.getFragment()); + } + return builder.toString(); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.java new file mode 100644 index 000000000000..308c71932913 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachehttpclient.v4_3; + +import java.net.URI; +import org.apache.http.client.methods.HttpRequestBase; + +final class HttpUriRequest extends HttpRequestBase { + + private final String methodName; + + HttpUriRequest(String methodName, URI uri) { + this.methodName = methodName; + setURI(uri); + } + + @Override + public String getMethod() { + return methodName; + } +} From cb24c1ed30bd95b883a57d69b3808a314dd74583 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Tue, 23 Sep 2025 16:56:46 -0400 Subject: [PATCH 2/5] docs --- .../documenting-instrumentation.md | 92 +++++++++++++++++++ instrumentation-docs/readme.md | 60 ++++++++++++ 2 files changed, 152 insertions(+) diff --git a/docs/contributing/documenting-instrumentation.md b/docs/contributing/documenting-instrumentation.md index 01b7acebd582..27640667de76 100644 --- a/docs/contributing/documenting-instrumentation.md +++ b/docs/contributing/documenting-instrumentation.md @@ -96,6 +96,22 @@ configurations: description: Enables statement sanitization for database queries. type: boolean default: true +override_telemetry: false +additional_telemetry: + - when: "default" + metrics: + - name: "metric.name" + description: "Metric description" + type: "COUNTER" + unit: "1" + attributes: + - name: "attribute.name" + type: "STRING" + spans: + - span_kind: "CLIENT" + attributes: + - name: "span.attribute" + type: "STRING" ``` ### Description (required) @@ -205,6 +221,82 @@ If an instrumentation is disabled by default, set `disabled_by_default: true`. T the instrumentation will not be active unless explicitly enabled by the user. If this field is omitted, it defaults to `false`, meaning the instrumentation is enabled by default. +### Manual Telemetry Documentation (optional) + +You can manually document telemetry metadata (metrics and spans) directly in the `metadata.yaml` file +using the `additional_telemetry` field. This is useful for: + +- Documenting telemetry that may not be captured during automated test runs +- Adding telemetry documentation when `.telemetry` files are not available +- Providing additional context or details about emitted telemetry + +#### additional_telemetry + +The `additional_telemetry` field allows you to specify telemetry metadata organized by configuration +conditions (`when` field): + +```yaml +additional_telemetry: + - when: "default" # Telemetry emitted by default + metrics: + - name: "http.server.request.duration" + description: "Duration of HTTP server requests" + type: "HISTOGRAM" + unit: "ms" + attributes: + - name: "http.method" + type: "STRING" + - name: "http.status_code" + type: "LONG" + spans: + - span_kind: "SERVER" + attributes: + - name: "http.method" + type: "STRING" + - name: "http.url" + type: "STRING" + - when: "otel.instrumentation.example.experimental-metrics.enabled" # Telemetry enabled by configuration + metrics: + - name: "example.experimental.metric" + description: "Experimental metric enabled by configuration" + type: "COUNTER" + unit: "1" +``` + +Each telemetry entry includes: + +- `when`: The configuration condition under which this telemetry is emitted. Use `"default"` for telemetry + emitted by default, or specify the configuration option name for conditional telemetry. +- `metrics`: List of metrics with their name, description, type, unit, and attributes +- `spans`: List of span configurations with their span_kind and attributes + +For metrics, supported `type` values include: `COUNTER`, `GAUGE`, `HISTOGRAM`, `EXPONENTIAL_HISTOGRAM`. + +For spans, supported `span_kind` values include: `CLIENT`, `SERVER`, `PRODUCER`, `CONSUMER`, `INTERNAL`. + +For attributes, supported `type` values include: `STRING`, `LONG`, `DOUBLE`, `BOOLEAN`. + +#### override_telemetry + +Set `override_telemetry: true` to completely replace any auto-generated telemetry data from `.telemetry` +files. When this is enabled, only the manually documented telemetry in `additional_telemetry` will be +used, and any `.telemetry` files will be ignored. + +```yaml +override_telemetry: true +additional_telemetry: + - when: "default" + metrics: + - name: "manually.documented.metric" + description: "This completely replaces auto-generated telemetry" + type: "GAUGE" + unit: "bytes" +``` + +If `override_telemetry` is `false` or omitted (default behavior), manual telemetry will be merged with +auto-generated telemetry, with manual entries taking precedence in case of conflicts (same metric name +or span kind within the same `when` condition). + ## Instrumentation List (docs/instrumentation-list.md) The contents of the `metadata.yaml` files are combined with other information about the instrumentation diff --git a/instrumentation-docs/readme.md b/instrumentation-docs/readme.md index c44e6b531941..53ec7d99d31e 100644 --- a/instrumentation-docs/readme.md +++ b/instrumentation-docs/readme.md @@ -181,6 +181,22 @@ configurations: description: Enables statement sanitization for database queries. type: boolean # boolean | string | list | map default: true +override_telemetry: false # Set to true to ignore auto-generated .telemetry files +additional_telemetry: # Manually document telemetry metadata + - when: "default" + metrics: + - name: "metric.name" + description: "Metric description" + type: "COUNTER" + unit: "1" + attributes: + - name: "attribute.name" + type: "STRING" + spans: + - span_kind: "CLIENT" + attributes: + - name: "span.attribute" + type: "STRING" ``` ### Gradle File Derived Information @@ -214,6 +230,50 @@ data will be excluded from git and just generated on demand. Each file has a `when` value along with the list of metrics that indicates whether the telemetry is emitted by default or via a configuration option. +#### Manual Telemetry Documentation + +In addition to auto-generated telemetry data from test runs, you can manually document telemetry +metadata directly in the `metadata.yaml` file. This is useful for: + +- Documenting telemetry that may not be captured during test runs +- Overriding auto-generated telemetry data when it's incomplete or incorrect +- Adding additional telemetry documentation that complements the auto-generated data + +You can add manual telemetry documentation using the `additional_telemetry` field: + +```yaml +additional_telemetry: + - when: "default" # or any configuration condition + metrics: + - name: "my.custom.metric" + description: "Description of the metric" + type: "COUNTER" + unit: "1" + attributes: + - name: "attribute.name" + type: "STRING" + spans: + - span_kind: "CLIENT" + attributes: + - name: "span.attribute" + type: "STRING" +``` + +To completely replace auto-generated telemetry data (ignoring `.telemetry` files), set `override_telemetry: true`: + +```yaml +override_telemetry: true +additional_telemetry: + - when: "default" + metrics: + - name: "documented.metric" + description: "This replaces all auto-generated metrics" + type: "GAUGE" + unit: "ms" +``` + +When both manual and auto-generated telemetry exist for the same `when` condition, they are merged with manual entries taking precedence in case of conflicts (same metric name or span kind). + ## Doc Synchronization The documentation site has a section that lists all the instrumentations in the context of From 5ba6965d5035cd0520cbf81a135141301f30843b Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Tue, 23 Sep 2025 16:59:32 -0400 Subject: [PATCH 3/5] fix naming --- .../docs/internal/TelemetryMerger.java | 6 +- .../docs/TelemetryMergerTest.java | 20 +- .../v4_3/AbstractApacheHttpClientTest.java | 328 ------------------ .../apachehttpclient/v4_3/HttpUriRequest.java | 24 -- 4 files changed, 13 insertions(+), 365 deletions(-) delete mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java delete mode 100644 instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.java diff --git a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/TelemetryMerger.java b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/TelemetryMerger.java index cd977513c900..9abbccc7229c 100644 --- a/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/TelemetryMerger.java +++ b/instrumentation-docs/src/main/java/io/opentelemetry/instrumentation/docs/internal/TelemetryMerger.java @@ -44,7 +44,7 @@ public static MergedTelemetryData merge( return convertManualTelemetryOnly(manualTelemetry); } - return mergeManualAndAutoGenerated( + return mergeManualAndEmitted( manualTelemetry, emittedMetrics, emittedSpans, instrumentationName); } @@ -75,13 +75,13 @@ private static MergedTelemetryData convertManualTelemetryOnly( * Merges manual telemetry with emitted telemetry, deduplicating by name within the same 'when' * condition. */ - private static MergedTelemetryData mergeManualAndAutoGenerated( + private static MergedTelemetryData mergeManualAndEmitted( List manualTelemetry, Map> emittedMetrics, Map> emittedSpans, String instrumentationName) { - // Start with generated data (create mutable copies of the lists) + // Start with emitted data (create mutable copies of the lists) Map> mergedMetrics = new HashMap<>(); for (Map.Entry> entry : emittedMetrics.entrySet()) { mergedMetrics.put(entry.getKey(), new ArrayList<>(entry.getValue())); diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/TelemetryMergerTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/TelemetryMergerTest.java index 169089dbaf73..a877743f2dd0 100644 --- a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/TelemetryMergerTest.java +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/TelemetryMergerTest.java @@ -25,8 +25,8 @@ void testOverrideMode() { List manualTelemetry = createManualTelemetry(); // Emitted telemetry that should be ignored - Map> autoMetrics = createAutoGeneratedMetrics(); - Map> autoSpans = createAutoGeneratedSpans(); + Map> autoMetrics = createEmittedMetrics(); + Map> autoSpans = createEmittedSpans(); TelemetryMerger.MergedTelemetryData result = TelemetryMerger.merge( @@ -53,8 +53,8 @@ void testOverrideMode() { void testMergeMode() { List manualTelemetry = createManualTelemetry(); - Map> autoMetrics = createAutoGeneratedMetrics(); - Map> autoSpans = createAutoGeneratedSpans(); + Map> autoMetrics = createEmittedMetrics(); + Map> autoSpans = createEmittedSpans(); TelemetryMerger.MergedTelemetryData result = TelemetryMerger.merge( @@ -80,8 +80,8 @@ void testConflictResolution() { // Create manual telemetry with same name as emitted List manualTelemetry = createConflictingManualTelemetry(); - Map> autoMetrics = createAutoGeneratedMetrics(); - Map> autoSpans = createAutoGeneratedSpans(); + Map> autoMetrics = createEmittedMetrics(); + Map> autoSpans = createEmittedSpans(); // Test merge mode with conflicts TelemetryMerger.MergedTelemetryData result = @@ -112,8 +112,8 @@ void testEmptyManualTelemetry() { // Empty manual telemetry List manualTelemetry = new ArrayList<>(); - Map> autoMetrics = createAutoGeneratedMetrics(); - Map> autoSpans = createAutoGeneratedSpans(); + Map> autoMetrics = createEmittedMetrics(); + Map> autoSpans = createEmittedSpans(); // Test merge mode with empty manual telemetry TelemetryMerger.MergedTelemetryData result = @@ -243,7 +243,7 @@ private static List createMultipleWhenManualTelemetry() { return manualTelemetry; } - private static Map> createAutoGeneratedMetrics() { + private static Map> createEmittedMetrics() { Map> autoMetrics = new HashMap<>(); List attrs = new ArrayList<>(); @@ -262,7 +262,7 @@ private static Map> createAutoGeneratedMetri return autoMetrics; } - private static Map> createAutoGeneratedSpans() { + private static Map> createEmittedSpans() { Map> autoSpans = new HashMap<>(); EmittedSpans.Span span = new EmittedSpans.Span(); diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java deleted file mode 100644 index 5d7d198d7193..000000000000 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/AbstractApacheHttpClientTest.java +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.apachehttpclient.v4_3; - -import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; -import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; -import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.URI; -import java.util.Map; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.client.ResponseHandler; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicHttpRequest; -import org.apache.http.protocol.BasicHttpContext; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.TestInstance; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public abstract class AbstractApacheHttpClientTest { - - protected abstract InstrumentationExtension testing(); - - protected abstract CloseableHttpClient createClient(boolean readTimeout); - - private CloseableHttpClient client; - private CloseableHttpClient clientWithReadTimeout; - - @BeforeAll - void setUp() { - client = createClient(false); - clientWithReadTimeout = createClient(true); - } - - @AfterAll - void tearDown() throws Exception { - client.close(); - clientWithReadTimeout.close(); - } - - CloseableHttpClient getClient(URI uri) { - if (uri.toString().contains("/read-timeout")) { - return clientWithReadTimeout; - } - return client; - } - - abstract static class ApacheHttpClientTest extends AbstractHttpClientTest { - @Override - protected void configure(HttpClientTestOptions.Builder optionsBuilder) { - optionsBuilder.markAsLowLevelInstrumentation(); - } - } - - @Nested - class ApacheClientHostRequestTest extends ApacheHttpClientTest { - - @Override - public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { - // also testing with an absolute path below - return configureRequest(new BasicHttpRequest(method, fullPathFromUri(uri)), headers); - } - - @Override - public int sendRequest( - BasicHttpRequest request, String method, URI uri, Map headers) - throws Exception { - return getResponseCode( - getClient(uri) - .execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request)); - } - - @Override - public void sendRequestWithCallback( - BasicHttpRequest request, - String method, - URI uri, - Map headers, - HttpClientResult httpClientResult) { - try { - getClient(uri) - .execute( - new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), - request, - responseCallback(httpClientResult)); - } catch (Throwable t) { - httpClientResult.complete(t); - } - } - } - - @Nested - class ApacheClientHostRequestContextTest extends ApacheHttpClientTest { - - @Override - public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { - // also testing with an absolute path below - return configureRequest(new BasicHttpRequest(method, fullPathFromUri(uri)), headers); - } - - @Override - public int sendRequest( - BasicHttpRequest request, String method, URI uri, Map headers) - throws Exception { - return getResponseCode( - getClient(uri) - .execute( - new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), - request, - new BasicHttpContext())); - } - - @Override - public void sendRequestWithCallback( - BasicHttpRequest request, - String method, - URI uri, - Map headers, - HttpClientResult httpClientResult) { - try { - getClient(uri) - .execute( - new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), - request, - responseCallback(httpClientResult), - new BasicHttpContext()); - } catch (Throwable t) { - httpClientResult.complete(t); - } - } - } - - @Nested - class ApacheClientHostAbsoluteUriRequestTest extends ApacheHttpClientTest { - - @Override - public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { - return configureRequest(new BasicHttpRequest(method, uri.toString()), headers); - } - - @Override - public int sendRequest( - BasicHttpRequest request, String method, URI uri, Map headers) - throws Exception { - return getResponseCode( - getClient(uri) - .execute(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), request)); - } - - @Override - public void sendRequestWithCallback( - BasicHttpRequest request, - String method, - URI uri, - Map headers, - HttpClientResult httpClientResult) { - try { - getClient(uri) - .execute( - new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), - request, - responseCallback(httpClientResult)); - } catch (Throwable t) { - httpClientResult.complete(t); - } - } - } - - @Nested - class ApacheClientHostAbsoluteUriRequestContextTest - extends ApacheHttpClientTest { - - @Override - public BasicHttpRequest buildRequest(String method, URI uri, Map headers) { - return configureRequest(new BasicHttpRequest(method, uri.toString()), headers); - } - - @Override - public int sendRequest( - BasicHttpRequest request, String method, URI uri, Map headers) - throws Exception { - return getResponseCode( - getClient(uri) - .execute( - new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), - request, - new BasicHttpContext())); - } - - @Override - public void sendRequestWithCallback( - BasicHttpRequest request, - String method, - URI uri, - Map headers, - HttpClientResult httpClientResult) { - try { - getClient(uri) - .execute( - new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), - request, - responseCallback(httpClientResult), - new BasicHttpContext()); - } catch (Throwable t) { - httpClientResult.complete(t); - } - } - } - - @Nested - class ApacheClientUriRequestTest extends ApacheHttpClientTest { - - @Override - public HttpUriRequest buildRequest(String method, URI uri, Map headers) { - // also testing with an absolute path below - return configureRequest(new HttpUriRequest(method, uri), headers); - } - - @Override - public int sendRequest( - HttpUriRequest request, String method, URI uri, Map headers) - throws Exception { - return getResponseCode(getClient(uri).execute(request)); - } - - @Override - public void sendRequestWithCallback( - HttpUriRequest request, - String method, - URI uri, - Map headers, - HttpClientResult httpClientResult) { - try { - getClient(uri).execute(request, responseCallback(httpClientResult)); - } catch (Throwable t) { - httpClientResult.complete(t); - } - } - } - - @Nested - class ApacheClientUriRequestContextTest extends ApacheHttpClientTest { - - @Override - public HttpUriRequest buildRequest(String method, URI uri, Map headers) { - // also testing with an absolute path below - return configureRequest(new HttpUriRequest(method, uri), headers); - } - - @Override - public int sendRequest( - HttpUriRequest request, String method, URI uri, Map headers) - throws Exception { - return getResponseCode(getClient(uri).execute(request, new BasicHttpContext())); - } - - @Override - public void sendRequestWithCallback( - HttpUriRequest request, - String method, - URI uri, - Map headers, - HttpClientResult httpClientResult) { - try { - getClient(uri).execute(request, responseCallback(httpClientResult), new BasicHttpContext()); - } catch (Throwable t) { - httpClientResult.complete(t); - } - } - } - - static T configureRequest(T request, Map headers) { - request.addHeader("user-agent", "apachehttpclient"); - headers.forEach((key, value) -> request.setHeader(new BasicHeader(key, value))); - return request; - } - - static int getResponseCode(HttpResponse response) { - try { - if (response.getEntity() != null && response.getEntity().getContent() != null) { - response.getEntity().getContent().close(); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return response.getStatusLine().getStatusCode(); - } - - static ResponseHandler responseCallback(HttpClientResult httpClientResult) { - return response -> { - try { - httpClientResult.complete(getResponseCode(response)); - } catch (Throwable t) { - httpClientResult.complete(t); - return response; - } - return response; - }; - } - - static String fullPathFromUri(URI uri) { - StringBuilder builder = new StringBuilder(); - if (uri.getPath() != null) { - builder.append(uri.getPath()); - } - - if (uri.getQuery() != null) { - builder.append('?'); - builder.append(uri.getQuery()); - } - - if (uri.getFragment() != null) { - builder.append('#'); - builder.append(uri.getFragment()); - } - return builder.toString(); - } -} diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.java b/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.java deleted file mode 100644 index 308c71932913..000000000000 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/library/src/test/java/io/opentelemetry/instrumentation/apachehttpclient/v4_3/HttpUriRequest.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.apachehttpclient.v4_3; - -import java.net.URI; -import org.apache.http.client.methods.HttpRequestBase; - -final class HttpUriRequest extends HttpRequestBase { - - private final String methodName; - - HttpUriRequest(String methodName, URI uri) { - this.methodName = methodName; - setURI(uri); - } - - @Override - public String getMethod() { - return methodName; - } -} From b28a8e0da77516bf7e4c24205881baed528b1f1a Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Tue, 23 Sep 2025 18:19:58 -0400 Subject: [PATCH 4/5] overrides --- instrumentation-docs/instrumentations.sh | 4 +- .../apache-httpclient-4.3/metadata.yaml | 39 +++++++++++++++++++ .../apache-httpclient-5.2/metadata.yaml | 39 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/instrumentation-docs/instrumentations.sh b/instrumentation-docs/instrumentations.sh index 17e365bd6e28..53a369f2b5b6 100755 --- a/instrumentation-docs/instrumentations.sh +++ b/instrumentation-docs/instrumentations.sh @@ -18,9 +18,9 @@ readonly INSTRUMENTATIONS=( "apache-httpasyncclient-4.1:javaagent:test" "apache-httpclient:apache-httpclient-2.0:javaagent:test" "apache-httpclient:apache-httpclient-4.0:javaagent:test" - "apache-httpclient:apache-httpclient-4.3:library:test" + # "apache-httpclient:apache-httpclient-4.3:library:test" # See https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/14771 "apache-httpclient:apache-httpclient-5.0:javaagent:test" - "apache-httpclient:apache-httpclient-5.2:library:test" + # "apache-httpclient:apache-httpclient-5.2:library:test" # See https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/14771 "armeria:armeria-1.3:javaagent:test" "armeria:armeria-grpc-1.14:javaagent:test" "async-http-client:async-http-client-1.9:javaagent:test" diff --git a/instrumentation/apache-httpclient/apache-httpclient-4.3/metadata.yaml b/instrumentation/apache-httpclient/apache-httpclient-4.3/metadata.yaml index 6dd728e55de2..f6e7736a4417 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-4.3/metadata.yaml +++ b/instrumentation/apache-httpclient/apache-httpclient-4.3/metadata.yaml @@ -4,3 +4,42 @@ library_link: https://hc.apache.org/index.html semantic_conventions: - HTTP_CLIENT_SPANS - HTTP_CLIENT_METRICS +additional_telemetry: +- when: default + metrics: + - name: http.client.request.duration + description: Duration of HTTP client requests. + type: HISTOGRAM + unit: s + attributes: + - name: http.request.method + type: STRING + - name: http.response.status_code + type: LONG + - name: network.protocol.version + type: STRING + - name: server.address + type: STRING + - name: server.port + type: LONG + spans: + - span_kind: CLIENT + attributes: + - name: error.type + type: STRING + - name: http.request.method + type: STRING + - name: http.request.method_original + type: STRING + - name: http.request.resend_count + type: LONG + - name: http.response.status_code + type: LONG + - name: network.protocol.version + type: STRING + - name: server.address + type: STRING + - name: server.port + type: LONG + - name: url.full + type: STRING diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.2/metadata.yaml b/instrumentation/apache-httpclient/apache-httpclient-5.2/metadata.yaml index 6dd728e55de2..f6e7736a4417 100644 --- a/instrumentation/apache-httpclient/apache-httpclient-5.2/metadata.yaml +++ b/instrumentation/apache-httpclient/apache-httpclient-5.2/metadata.yaml @@ -4,3 +4,42 @@ library_link: https://hc.apache.org/index.html semantic_conventions: - HTTP_CLIENT_SPANS - HTTP_CLIENT_METRICS +additional_telemetry: +- when: default + metrics: + - name: http.client.request.duration + description: Duration of HTTP client requests. + type: HISTOGRAM + unit: s + attributes: + - name: http.request.method + type: STRING + - name: http.response.status_code + type: LONG + - name: network.protocol.version + type: STRING + - name: server.address + type: STRING + - name: server.port + type: LONG + spans: + - span_kind: CLIENT + attributes: + - name: error.type + type: STRING + - name: http.request.method + type: STRING + - name: http.request.method_original + type: STRING + - name: http.request.resend_count + type: LONG + - name: http.response.status_code + type: LONG + - name: network.protocol.version + type: STRING + - name: server.address + type: STRING + - name: server.port + type: LONG + - name: url.full + type: STRING From d80aa4f964a6b8911bdb05c59e1000554813ea45 Mon Sep 17 00:00:00 2001 From: Jay DeLuca Date: Wed, 24 Sep 2025 16:11:30 -0400 Subject: [PATCH 5/5] refactor test assertion approach --- .../docs/ManualTelemetryTest.java | 138 +++++++++++++----- 1 file changed, 99 insertions(+), 39 deletions(-) diff --git a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/ManualTelemetryTest.java b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/ManualTelemetryTest.java index 7a60bc9cf201..ebe15067b2e5 100644 --- a/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/ManualTelemetryTest.java +++ b/instrumentation-docs/src/test/java/io/opentelemetry/instrumentation/docs/ManualTelemetryTest.java @@ -10,7 +10,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.opentelemetry.instrumentation.docs.internal.InstrumentationMetadata; import io.opentelemetry.instrumentation.docs.internal.ManualTelemetryEntry; +import io.opentelemetry.instrumentation.docs.internal.SemanticConvention; +import io.opentelemetry.instrumentation.docs.internal.TelemetryAttribute; import io.opentelemetry.instrumentation.docs.utils.YamlHelper; +import java.util.List; import org.junit.jupiter.api.Test; class ManualTelemetryTest { @@ -51,36 +54,45 @@ void testManualTelemetryParsing() throws JsonProcessingException { type: STRING """; - InstrumentationMetadata metadata = YamlHelper.metaDataParser(yamlContent); - - assertThat(metadata).isNotNull(); - assertThat(metadata.getDescription()) - .isEqualTo("Example instrumentation with manual telemetry documentation"); - assertThat(metadata.getLibraryLink()).isEqualTo("https://example.com/library"); - assertThat(metadata.getOverrideTelemetry()).isFalse(); - - assertThat(metadata.getAdditionalTelemetry()).hasSize(2); - - ManualTelemetryEntry defaultEntry = metadata.getAdditionalTelemetry().get(0); - assertThat(defaultEntry.getWhen()).isEqualTo("default"); - assertThat(defaultEntry.getMetrics()).hasSize(1); - assertThat(defaultEntry.getSpans()).hasSize(1); - - ManualTelemetryEntry.ManualMetric metric = defaultEntry.getMetrics().get(0); - assertThat(metric.getName()).isEqualTo("system.disk.io"); - assertThat(metric.getDescription()).isEqualTo("System disk IO"); - assertThat(metric.getType()).isEqualTo("LONG_SUM"); - assertThat(metric.getUnit()).isEqualTo("By"); - assertThat(metric.getAttributes()).hasSize(2); - - ManualTelemetryEntry.ManualSpan span = defaultEntry.getSpans().get(0); - assertThat(span.getSpanKind()).isEqualTo("CLIENT"); - assertThat(span.getAttributes()).hasSize(1); - - ManualTelemetryEntry experimentalEntry = metadata.getAdditionalTelemetry().get(1); - assertThat(experimentalEntry.getWhen()).isEqualTo("experimental"); - assertThat(experimentalEntry.getMetrics()).hasSize(1); - assertThat(experimentalEntry.getSpans()).isEmpty(); + InstrumentationMetadata actualMetadata = YamlHelper.metaDataParser(yamlContent); + ManualTelemetryEntry defaultEntry = + new ManualTelemetryEntry( + "default", + List.of( + new ManualTelemetryEntry.ManualMetric( + "system.disk.io", + "System disk IO", + "LONG_SUM", + "By", + List.of( + new TelemetryAttribute("device", "STRING"), + new TelemetryAttribute("direction", "STRING")))), + List.of( + new ManualTelemetryEntry.ManualSpan( + "CLIENT", List.of(new TelemetryAttribute("custom.operation", "STRING"))))); + + ManualTelemetryEntry experimentalEntry = + new ManualTelemetryEntry( + "experimental", + List.of( + new ManualTelemetryEntry.ManualMetric( + "experimental.feature.usage", + "Usage of experimental features", + "HISTOGRAM", + "s", + List.of(new TelemetryAttribute("feature.name", "STRING")))), + List.of()); + + InstrumentationMetadata expectedMetadata = + new InstrumentationMetadata.Builder() + .description("Example instrumentation with manual telemetry documentation") + .libraryLink("https://example.com/library") + .semanticConventions(List.of(SemanticConvention.HTTP_CLIENT_SPANS)) + .additionalTelemetry(List.of(defaultEntry, experimentalEntry)) + .overrideTelemetry(false) + .build(); + + assertThat(actualMetadata).usingRecursiveComparison().isEqualTo(expectedMetadata); } @Test @@ -99,11 +111,24 @@ void testOverrideTelemetryFlag() throws JsonProcessingException { attributes: [] """; - InstrumentationMetadata metadata = YamlHelper.metaDataParser(yamlContent); + InstrumentationMetadata actualMetadata = YamlHelper.metaDataParser(yamlContent); + + ManualTelemetryEntry defaultEntry = + new ManualTelemetryEntry( + "default", + List.of( + new ManualTelemetryEntry.ManualMetric( + "manual.metric", "Manual metric only", "COUNTER", "1", List.of())), + List.of()); - assertThat(metadata).isNotNull(); - assertThat(metadata.getOverrideTelemetry()).isTrue(); - assertThat(metadata.getAdditionalTelemetry()).hasSize(1); + InstrumentationMetadata expectedMetadata = + new InstrumentationMetadata.Builder() + .description("Example with override") + .overrideTelemetry(true) + .additionalTelemetry(List.of(defaultEntry)) + .build(); + + assertThat(actualMetadata).usingRecursiveComparison().isEqualTo(expectedMetadata); } @Test @@ -115,10 +140,45 @@ void testEmptyAdditionalTelemetry() throws JsonProcessingException { - HTTP_CLIENT_SPANS """; - InstrumentationMetadata metadata = YamlHelper.metaDataParser(yamlContent); - - assertThat(metadata).isNotNull(); - assertThat(metadata.getOverrideTelemetry()).isFalse(); - assertThat(metadata.getAdditionalTelemetry()).isEmpty(); + InstrumentationMetadata actualMetadata = YamlHelper.metaDataParser(yamlContent); + + ManualTelemetryEntry defaultEntry = + new ManualTelemetryEntry( + "default", + List.of( + new ManualTelemetryEntry.ManualMetric( + "system.disk.io", + "System disk IO", + "LONG_SUM", + "By", + List.of( + new TelemetryAttribute("device", "STRING"), + new TelemetryAttribute("direction", "STRING")))), + List.of( + new ManualTelemetryEntry.ManualSpan( + "CLIENT", List.of(new TelemetryAttribute("custom.operation", "STRING"))))); + + ManualTelemetryEntry experimentalEntry = + new ManualTelemetryEntry( + "experimental", + List.of( + new ManualTelemetryEntry.ManualMetric( + "experimental.feature.usage", + "Usage of experimental features", + "HISTOGRAM", + "s", + List.of(new TelemetryAttribute("feature.name", "STRING")))), + List.of()); + + InstrumentationMetadata expectedMetadata = + new InstrumentationMetadata.Builder() + .description("Example instrumentation with manual telemetry documentation") + .libraryLink("https://example.com/library") + .semanticConventions(List.of(SemanticConvention.HTTP_CLIENT_SPANS)) + .additionalTelemetry(List.of(defaultEntry, experimentalEntry)) + .overrideTelemetry(false) + .build(); + + assertThat(actualMetadata).usingRecursiveComparison().isEqualTo(expectedMetadata); } }