Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
4bb3e56
Add implementation for new exporter health metrics
JonasKunz Apr 9, 2025
dd651c3
Use new implementation in GRPC exporters
JonasKunz Apr 9, 2025
20959d8
Use new implementation in HTTP exporters
JonasKunz Apr 10, 2025
4bafbfa
Use new implementation in zipkin exporter
JonasKunz Apr 10, 2025
67fe9ec
Merge remote-tracking branch 'otel/main' into health-metrics
JonasKunz Apr 10, 2025
073ecee
add javadoc since tags
JonasKunz Apr 10, 2025
ba07867
spotless
JonasKunz Apr 10, 2025
5281d9a
Fix javadoc and style issues
JonasKunz Apr 10, 2025
e560424
Added test for HTTP Exporter
JonasKunz Apr 10, 2025
2f29ac2
Implemented duration metric
JonasKunz Apr 10, 2025
a2a78f7
spotless
JonasKunz Apr 10, 2025
0bfa4f4
Added GRPC test
JonasKunz Apr 11, 2025
149ffbd
Added server.* attributes
JonasKunz Apr 11, 2025
22ba608
Added tests for server.* attribute extraction
JonasKunz Apr 16, 2025
8522179
Adjust metric names to now merged semconv PR
JonasKunz Apr 17, 2025
d4afdc9
Merge remote-tracking branch 'otel/HEAD' into health-metrics
JonasKunz Apr 17, 2025
9adacd0
spotless
JonasKunz Apr 17, 2025
676b66c
Implement status code attributes
JonasKunz Apr 17, 2025
6d5bf61
Merge remote-tracking branch 'otel/main' into health-metrics
JonasKunz Apr 28, 2025
ec686d3
Renames
JonasKunz May 5, 2025
ad69fa5
Introduce interface as abstraction
JonasKunz May 5, 2025
a1cd746
Complete renaming of "HealthMetricLevel"
JonasKunz May 5, 2025
48b6320
Remove since tags
JonasKunz May 6, 2025
32943e1
Add tests for semconv attributes
JonasKunz May 6, 2025
d2db75d
Move standard component types to enum
JonasKunz May 6, 2025
5f94bb5
spotless
JonasKunz May 6, 2025
5b71238
Fix compilation
JonasKunz May 6, 2025
ba4dc7e
Merge remote-tracking branch 'otel/main' into health-metrics
JonasKunz May 6, 2025
bd6e410
Fix checks
JonasKunz May 6, 2025
7720b8d
Style fixes
JonasKunz May 6, 2025
68967e7
Move attribute references to instrumentation api
JonasKunz May 6, 2025
1b4948b
Fix tests
JonasKunz May 6, 2025
02aafc1
Fix http sender tests
JonasKunz May 6, 2025
923506e
Add E2E exporter tests
JonasKunz May 7, 2025
7360b62
Merge remote-tracking branch 'otel/HEAD' into health-metrics
JonasKunz May 7, 2025
05d1fba
Self-review fixes
JonasKunz May 7, 2025
e84ea68
Move ServerAttributesUtil call down
JonasKunz May 20, 2025
b919c7e
Remove StandardType interface
JonasKunz May 20, 2025
34cd9ec
Move Signal to upper level
JonasKunz May 20, 2025
374d214
Move namespace and unit to signal enum
JonasKunz May 20, 2025
a5f3178
Move signal to standalone enum
JonasKunz May 20, 2025
7fbf2db
Inline unnecessary ComponentId.put method
JonasKunz May 20, 2025
66cd86e
Add StandardComponentId to not have to pass the type around separately
JonasKunz May 20, 2025
4202870
Merge remote-tracking branch 'otel/main' into health-metrics
JonasKunz May 20, 2025
c5051ed
Rename InternalTelemetrySchemaVersion and its setters
JonasKunz May 20, 2025
1b037a5
Remove DISABLED telemetry schema version
JonasKunz May 20, 2025
3a89f42
Rename and move standard exporter types
JonasKunz May 20, 2025
aa96250
Exclude profiles exporter from metrics
JonasKunz May 20, 2025
b472345
apidiff
JonasKunz May 20, 2025
7d9b16e
fix japicmp
JonasKunz May 20, 2025
51edc50
Update exporters/common/src/test/java/io/opentelemetry/exporter/inter…
JonasKunz May 28, 2025
8ff0381
Update exporters/common/src/test/java/io/opentelemetry/exporter/inter…
JonasKunz May 28, 2025
a52d607
Review fixes
JonasKunz May 28, 2025
165f9a6
Merge remote-tracking branch 'otel/main' into health-metrics
JonasKunz May 28, 2025
03496e8
Use assum to early-out of test
JonasKunz May 28, 2025
b53cbe9
remove Signal.toString
JonasKunz May 30, 2025
6350dff
remove StandardComponentId.toString
JonasKunz May 30, 2025
e307e60
Remove InternalTelemetryVersion.V1_33
JonasKunz May 30, 2025
00b4f2f
spotless
JonasKunz May 30, 2025
8e89011
japicmp
jack-berg Jun 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions exporters/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
compileOnly("io.grpc:grpc-stub")

testImplementation(project(":sdk:common"))
testImplementation(project(":sdk:testing"))

testImplementation("com.google.protobuf:protobuf-java-util")
testImplementation("com.linecorp.armeria:armeria-junit5")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,132 +5,268 @@

package io.opentelemetry.exporter.internal;

import static io.opentelemetry.api.common.AttributeKey.booleanKey;
import static io.opentelemetry.api.common.AttributeKey.stringKey;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.LongUpDownCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.sdk.common.Clock;
import io.opentelemetry.sdk.common.HealthMetricLevel;
import io.opentelemetry.sdk.internal.ComponentId;
import io.opentelemetry.sdk.internal.SemConvAttributes;
import java.util.Collections;
import java.util.function.Supplier;
import javax.annotation.Nullable;

/**
* Helper for recording metrics from exporters.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public class ExporterMetrics {

private static final AttributeKey<String> ATTRIBUTE_KEY_TYPE = stringKey("type");
private static final AttributeKey<Boolean> ATTRIBUTE_KEY_SUCCESS = booleanKey("success");
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity did the build force this comment to pass?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes:

> Compilation failed; see the compiler output below.
  /Users/jonas/git/otel/opentelemetry-java/exporters/common/src/main/java/io/opentelemetry/exporter/internal/ExporterMetrics.java:29: warning: [OtelInternalJavadoc] This public internal class doesn't end with any of the applicable javadoc disclaimers: "This class is internal and is hence not for public use. Its APIs are unstable and can change at any time.", or "This class is internal and experimental. Its APIs are unstable and can change at any time. Its APIs (or a version of them) may be promoted to the public stable API in the future, but no guarantees are made."
    public enum Signal {
           ^
      (see https://errorprone.info/bugpattern/OtelInternalJavadoc)
  1 error
  1 warning

Is that not intentional?

*/
public enum Signal {
SPAN("span", "span"),
METRIC("metric_data_point", "data_point"),
LOG("log", "log_record");

private final String namespace;
private final String unit;

Signal(String namespace, String unit) {
this.namespace = namespace;
this.unit = unit;
}

@Override
public String toString() {
return namespace;
}
}

private static final Clock CLOCK = Clock.getDefault();

private final Supplier<MeterProvider> meterProviderSupplier;
private final String exporterName;
private final String transportName;
private final Attributes seenAttrs;
private final Attributes successAttrs;
private final Attributes failedAttrs;
private final Signal signal;
private final ComponentId componentId;
private final Attributes additionalAttributes;
private final boolean enabled;

/** Access via {@link #seen()}. */
@Nullable private volatile LongCounter seen;
@Nullable private volatile LongUpDownCounter inflight = null;
@Nullable private volatile LongCounter exported = null;
@Nullable private volatile DoubleHistogram duration = null;
@Nullable private volatile Attributes allAttributes = null;

/** Access via {@link #exported()} . */
@Nullable private volatile LongCounter exported;
public ExporterMetrics(
HealthMetricLevel level,
Supplier<MeterProvider> meterProviderSupplier,
Signal signal,
ComponentId componentId) {
this(level, meterProviderSupplier, signal, componentId, null);
}

private ExporterMetrics(
public ExporterMetrics(
HealthMetricLevel level,
Supplier<MeterProvider> meterProviderSupplier,
String exporterName,
String type,
String transportName) {
Signal signal,
ComponentId componentId,
@Nullable Attributes additionalAttributes) {
switch (level) {
case ON:
enabled = true;
break;
case OFF:
case LEGACY:
enabled = false;
break;
default:
throw new IllegalArgumentException("Unhandled case " + level);
}
;

this.meterProviderSupplier = meterProviderSupplier;
this.exporterName = exporterName;
this.transportName = transportName;
this.seenAttrs = Attributes.builder().put(ATTRIBUTE_KEY_TYPE, type).build();
this.successAttrs = this.seenAttrs.toBuilder().put(ATTRIBUTE_KEY_SUCCESS, true).build();
this.failedAttrs = this.seenAttrs.toBuilder().put(ATTRIBUTE_KEY_SUCCESS, false).build();
this.componentId = componentId;
this.signal = signal;
if (additionalAttributes != null) {
this.additionalAttributes = additionalAttributes;
} else {
this.additionalAttributes = Attributes.empty();
}
}

/** Record number of records seen. */
public void addSeen(long value) {
seen().add(value, seenAttrs);
public Recording startRecordingExport(int itemCount) {
return new Recording(itemCount);
}

/** Record number of records which successfully exported. */
public void addSuccess(long value) {
exported().add(value, successAttrs);
private Meter meter() {
return meterProviderSupplier
.get()
.get("io.opentelemetry.exporters." + componentId.getTypeName());
}

/** Record number of records which failed to export. */
public void addFailed(long value) {
exported().add(value, failedAttrs);
private Attributes allAttributes() {
// attributes are initialized lazily to trigger lazy initialization of the componentId
Attributes allAttributes = this.allAttributes;
if (allAttributes == null) {
AttributesBuilder builder = Attributes.builder();
componentId.put(builder);
builder.putAll(additionalAttributes);
allAttributes = builder.build();
this.allAttributes = allAttributes;
}
return allAttributes;
}

private LongCounter seen() {
LongCounter seen = this.seen;
if (seen == null || isNoop(seen)) {
seen = meter().counterBuilder(exporterName + ".exporter.seen").build();
this.seen = seen;
private LongUpDownCounter inflight() {
LongUpDownCounter inflight = this.inflight;
if (inflight == null || isNoop(inflight)) {
inflight =
meter()
.upDownCounterBuilder("otel.sdk.exporter." + signal.namespace + ".inflight")
.setUnit("{" + signal.unit + "}")
.setDescription(
"The number of "
+ signal.unit
+ "s which were passed to the exporter, but that have not been exported yet (neither successful, nor failed)")
.build();
this.inflight = inflight;
}
return seen;
return inflight;
}

private LongCounter exported() {
LongCounter exported = this.exported;
if (exported == null || isNoop(exported)) {
exported = meter().counterBuilder(exporterName + ".exporter.exported").build();
exported =
meter()
.counterBuilder("otel.sdk.exporter." + signal.namespace + ".exported")
.setUnit("{" + signal.unit + "}")
.setDescription(
"The number of "
+ signal.unit
+ "s for which the export has finished, either successful or failed")
.build();
this.exported = exported;
}
return exported;
}

private Meter meter() {
return meterProviderSupplier
.get()
.get("io.opentelemetry.exporters." + exporterName + "-" + transportName);
private DoubleHistogram duration() {
DoubleHistogram duration = this.duration;
if (duration == null || isNoop(duration)) {
duration =
meter()
.histogramBuilder("otel.sdk.exporter.operation.duration")
.setUnit("s")
.setDescription("The duration of exporting a batch of telemetry records")
.setExplicitBucketBoundariesAdvice(Collections.emptyList())
.build();
this.duration = duration;
}
return duration;
}

private void incrementInflight(long count) {
if (!enabled) {
return;
}
inflight().add(count, allAttributes());
}

private static boolean isNoop(LongCounter counter) {
private void decrementInflight(long count) {
if (!enabled) {
return;
}
inflight().add(-count, allAttributes());
}

private void incrementExported(long count, @Nullable String errorType) {
if (!enabled) {
return;
}
exported().add(count, getAttributesWithPotentialError(errorType, Attributes.empty()));
}

static boolean isNoop(Object instrument) {
// This is a poor way to identify a Noop implementation, but the API doesn't provide a better
// way. Perhaps we could add a common "Noop" interface to allow for an instanceof check?
return counter.getClass().getSimpleName().startsWith("Noop");
return instrument.getClass().getSimpleName().startsWith("Noop");
}

/**
* Create an instance for recording exporter metrics under the meter {@code
* "io.opentelemetry.exporters." + exporterName + "-grpc}".
*/
public static ExporterMetrics createGrpc(
String exporterName, String type, Supplier<MeterProvider> meterProvider) {
return new ExporterMetrics(meterProvider, exporterName, type, "grpc");
private Attributes getAttributesWithPotentialError(
@Nullable String errorType, Attributes additionalAttributes) {
Attributes attributes = allAttributes();
boolean errorPresent = errorType != null && !errorType.isEmpty();
if (errorPresent || !additionalAttributes.isEmpty()) {
AttributesBuilder builder = attributes.toBuilder();
if (errorPresent) {
builder.put(SemConvAttributes.ERROR_TYPE, errorType);
}
attributes = builder.putAll(additionalAttributes).build();
}
return attributes;
}

/**
* Create an instance for recording exporter metrics under the meter {@code
* "io.opentelemetry.exporters." + exporterName + "-grpc-okhttp}".
*/
public static ExporterMetrics createGrpcOkHttp(
String exporterName, String type, Supplier<MeterProvider> meterProvider) {
return new ExporterMetrics(meterProvider, exporterName, type, "grpc-okhttp");
private void recordDuration(
double seconds, @Nullable String errorType, Attributes requestAttributes) {
if (!enabled) {
return;
}
duration().record(seconds, getAttributesWithPotentialError(errorType, requestAttributes));
}

/**
* Create an instance for recording exporter metrics under the meter {@code
* "io.opentelemetry.exporters." + exporterName + "-http}".
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public static ExporterMetrics createHttpProtobuf(
String exporterName, String type, Supplier<MeterProvider> meterProvider) {
return new ExporterMetrics(meterProvider, exporterName, type, "http");
}
public class Recording {
/** The number items (spans, log records or metric data points) being exported */
private final int itemCount;

/**
* Create an instance for recording exporter metrics under the meter {@code
* "io.opentelemetry.exporters." + exporterName + "-http-json}".
*/
public static ExporterMetrics createHttpJson(
String exporterName, String type, Supplier<MeterProvider> meterProvider) {
return new ExporterMetrics(meterProvider, exporterName, type, "http-json");
private final long startNanoTime;

private boolean alreadyEnded = false;

private Recording(int itemCount) {
this.itemCount = itemCount;
startNanoTime = CLOCK.nanoTime();
incrementInflight(itemCount);
}

public void finishSuccessful(Attributes requestAttributes) {
finish(0, null, requestAttributes);
}

public void finishFailed(String errorReason, Attributes requestAttributes) {
finish(itemCount, errorReason, requestAttributes);
}

private void finish(int failedCount, @Nullable String errorType, Attributes requestAttributes) {
if (alreadyEnded) {
throw new IllegalStateException("Recording already ended");
}
alreadyEnded = true;

decrementInflight(itemCount);

if (failedCount > 0) {
if (errorType == null || errorType.isEmpty()) {
throw new IllegalArgumentException(
"Some items failed but no failure reason was provided");
}
incrementExported(failedCount, errorType);
}
int successCount = itemCount - failedCount;
if (successCount > 0) {
incrementExported(successCount, null);
}
long durationNanos = CLOCK.nanoTime() - startNanoTime;
recordDuration(durationNanos / 1_000_000_000.0, errorType, requestAttributes);
}
}
}
Loading
Loading