From abcb2ecf494418b1d126628541cb1215e3f3ecab Mon Sep 17 00:00:00 2001 From: Patrik Beqo Date: Wed, 14 Oct 2026 10:16:39 -0400 Subject: [PATCH] Make the OpenTelemetry v2 logger provider replay safe --- CHANGELOG.md | 4 +- contrib/temporal-opentelemetry-v2/README.md | 164 +++++++++++++++++ .../temporal-opentelemetry-v2/build.gradle | 5 +- .../v2/ReplaySafeOpenTelemetry.java | 65 ++++++- .../v2/internal/ReplaySafeLogger.java | 170 ++++++++++++++++++ .../temporal/opentelemetry/v2/LoggerTest.java | 116 ++++++++++++ .../opentelemetry/v2/OtelTestBase.java | 17 +- .../src/test/resources/logback-test.xml | 17 ++ 8 files changed, 545 insertions(+), 13 deletions(-) create mode 100644 contrib/temporal-opentelemetry-v2/README.md create mode 100644 contrib/temporal-opentelemetry-v2/src/main/java/io/temporal/opentelemetry/v2/internal/ReplaySafeLogger.java create mode 100644 contrib/temporal-opentelemetry-v2/src/test/java/io/temporal/opentelemetry/v2/LoggerTest.java create mode 100644 contrib/temporal-opentelemetry-v2/src/test/resources/logback-test.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index e30d8033ce..bb5ae678aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,8 +36,8 @@ to docs, or any other relevant information. `addScheduleClientInterceptors`, `addActivityClientInterceptors`, and `addNexusClientInterceptors`. - Added the experimental `temporal-opentelemetry-v2` module. `OpenTelemetryPlugin` propagates OpenTelemetry context through Temporal headers and can optionally create spans for Temporal operations. `ReplaySafeOpenTelemetry` makes - workflow spans replay safe with deterministic span and trace IDs, and drops metric recordings made by replaying - workflow code. + workflow spans replay safe with deterministic span and trace IDs, and drops metric recordings and log records made + by replaying workflow code. ### Changed - Release notes for all future releases are now in a single CHANGELOG.md file. `releases` directory with old release diff --git a/contrib/temporal-opentelemetry-v2/README.md b/contrib/temporal-opentelemetry-v2/README.md new file mode 100644 index 0000000000..76acde1c11 --- /dev/null +++ b/contrib/temporal-opentelemetry-v2/README.md @@ -0,0 +1,164 @@ +# OpenTelemetry v2 integration for the Temporal Java SDK + +Module `io.temporal:temporal-opentelemetry-v2` provides replay-safe +OpenTelemetry tracing, metrics, and logs for Temporal. + +> [!WARNING] +> This module is experimental, and its API may change. + +## Setup + +Use the same version as the rest of your Temporal Java SDK dependencies: + +```groovy +implementation 'io.temporal:temporal-opentelemetry-v2:' +// Add the exporters you use, such as this OTLP exporter. +implementation 'io.opentelemetry:opentelemetry-exporter-otlp' +``` + +Create a replay-safe OpenTelemetry instance, register it as the global, and +attach the plugin to your service stubs: + +```java +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.temporal.client.WorkflowClient; +import io.temporal.opentelemetry.v2.OpenTelemetryPlugin; +import io.temporal.opentelemetry.v2.ReplaySafeOpenTelemetry; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.worker.WorkerFactory; + +ReplaySafeOpenTelemetry openTelemetry = + ReplaySafeOpenTelemetry.newBuilder() + .setTracerProviderBuilder( + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder(OtlpGrpcSpanExporter.builder().build()).build())) + .setMeterProviderBuilder( + SdkMeterProvider.builder() + .registerMetricReader( + PeriodicMetricReader.builder(OtlpGrpcMetricExporter.builder().build()) + .build())) + .setLoggerProviderBuilder( + SdkLoggerProvider.builder() + .addLogRecordProcessor( + BatchLogRecordProcessor.builder(OtlpGrpcLogRecordExporter.builder().build()) + .build())) + .build(); +GlobalOpenTelemetry.set(openTelemetry); + +WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs( + WorkflowServiceStubsOptions.newBuilder() + .setPlugins(OpenTelemetryPlugin.newBuilder().build()) + .build()); + +WorkflowClient client = WorkflowClient.newInstance(service); +WorkerFactory factory = WorkerFactory.newInstance(client); +``` + +Plugins configured on `WorkflowServiceStubsOptions` propagate to clients and +workers created from those stubs. It can also be configured directly on +`WorkflowClientOptions`. + +Build `ReplaySafeOpenTelemetry` before anything else uses the OpenTelemetry +`Context`, and register it as the global before building the plugin. Otherwise, +`OpenTelemetryPlugin.Builder.build()` throws `IllegalStateException`. + +Call `ReplaySafeOpenTelemetry.close()` after your workers and clients have +stopped to export buffered telemetry and shut down its providers. + +## Tracing + +The plugin propagates application trace context through Temporal headers. +Application spans remain connected across clients, workflows, activities, and +Nexus operations. + +Set `OpenTelemetryPlugin.Builder.setAddTemporalSpans(true)` to emit spans for +operations such as `StartWorkflow`, `RunWorkflow`, `RunActivity`, and +`ContinueAsNew`. + +Create spans in workflow, client, and activity code with the standard OpenTelemetry API: + +```java +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; + +Span span = + GlobalOpenTelemetry.getTracer("my-workflows") + .spanBuilder("my-span") + .startSpan(); +try (Scope ignored = span.makeCurrent()) { + activity.doWork(); +} finally { + span.end(); +} +``` + +## Metrics + +Create metrics in workflow, client, and activity code with the standard +OpenTelemetry API. Workflow code supports only synchronous instruments: + +```java +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.LongCounter; + +LongCounter counter = + GlobalOpenTelemetry.getMeter("my-workflows") + .counterBuilder("workflow.items.processed") + .build(); +counter.add(1); +``` + +## Logs + +Create logs in workflow, client, and activity code with SLF4J, and send them to +OpenTelemetry by installing a logging appender, such as the +[OpenTelemetry Logback appender](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-appender-1.0/library), +with the replay-safe instance. The appender drops records from replaying +workflow code only when it is installed with `ReplaySafeOpenTelemetry`, not a +plain `OpenTelemetrySdk`: + +```java +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; +import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.temporal.opentelemetry.v2.ReplaySafeOpenTelemetry; + +ReplaySafeOpenTelemetry openTelemetry = + ReplaySafeOpenTelemetry.newBuilder() + .setLoggerProviderBuilder( + SdkLoggerProvider.builder() + .addLogRecordProcessor( + BatchLogRecordProcessor.builder(OtlpGrpcLogRecordExporter.builder().build()) + .build())) + .build(); +OpenTelemetryAppender.install(openTelemetry); +``` + +Then log with SLF4J: + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +Logger logger = LoggerFactory.getLogger(MyWorkflowImpl.class); +logger.info("workflow step completed"); +``` + +Configure the appender as its setup guide describes. Replay is checked on the +thread that emits the record, so behind an asynchronous appender, such as +Logback's `AsyncAppender`, log from workflow code with `Workflow.getLogger()`, +which skips logging during replay. diff --git a/contrib/temporal-opentelemetry-v2/build.gradle b/contrib/temporal-opentelemetry-v2/build.gradle index 3fb51d1651..bc4000f89c 100644 --- a/contrib/temporal-opentelemetry-v2/build.gradle +++ b/contrib/temporal-opentelemetry-v2/build.gradle @@ -2,6 +2,7 @@ description = '''Temporal Java SDK OpenTelemetry v2 Module''' ext { otelVersion = '1.66.0' + otelInstrumentationVersion = '2.32.0-alpha' } dependencies { @@ -22,7 +23,7 @@ dependencies { testImplementation project(':temporal-serviceclient') testImplementation project(':temporal-testing') testImplementation "io.opentelemetry:opentelemetry-sdk-testing" + testImplementation "io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0:$otelInstrumentationVersion" testImplementation "junit:junit:${junitVersion}" - - testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}" + testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}" } diff --git a/contrib/temporal-opentelemetry-v2/src/main/java/io/temporal/opentelemetry/v2/ReplaySafeOpenTelemetry.java b/contrib/temporal-opentelemetry-v2/src/main/java/io/temporal/opentelemetry/v2/ReplaySafeOpenTelemetry.java index 1bccd9be0b..953feb9896 100644 --- a/contrib/temporal-opentelemetry-v2/src/main/java/io/temporal/opentelemetry/v2/ReplaySafeOpenTelemetry.java +++ b/contrib/temporal-opentelemetry-v2/src/main/java/io/temporal/opentelemetry/v2/ReplaySafeOpenTelemetry.java @@ -3,6 +3,8 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerBuilder; import io.opentelemetry.api.logs.LoggerProvider; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.MeterBuilder; @@ -23,6 +25,7 @@ import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; import io.temporal.common.Experimental; import io.temporal.opentelemetry.v2.internal.ReplaySafeIdGenerator; +import io.temporal.opentelemetry.v2.internal.ReplaySafeLogger; import io.temporal.opentelemetry.v2.internal.ReplaySafeMeter; import io.temporal.opentelemetry.v2.internal.ReplaySafeTracer; import io.temporal.opentelemetry.v2.internal.TemporalContextStorage; @@ -31,8 +34,8 @@ /** * The {@link OpenTelemetry} to use for OpenTelemetry integration with Temporal. Register it with - * {@code GlobalOpenTelemetry.set}; tracers and meters obtained from it are replay safe inside - * workflows. + * {@code GlobalOpenTelemetry.set}; tracers, meters, and loggers obtained from it are replay safe + * inside workflows. * *

Known limitations: * @@ -51,17 +54,15 @@ * Promise.allOf} and {@code Promise.anyOf}, run with the context current when the promise * completes. *

  • Continuations on the promise returned by cancelling an external workflow run outside a - * workflow thread, so spans they start get random IDs, and their spans and metric recordings - * are exported again on every replay. + * workflow thread, so spans they start get random IDs, and their spans, metric recordings, + * and log records are exported again on every replay. * */ @Experimental public final class ReplaySafeOpenTelemetry implements OpenTelemetry, Closeable { private final ReplaySafeTracerProvider tracerProvider; private final ReplaySafeMeterProvider meterProvider; - // TODO: Make the logger provider replay safe and add logger interceptor methods for Temporal and - // OpenTelemetry loggers. - private final SdkLoggerProvider loggerProvider; + private final ReplaySafeLoggerProvider loggerProvider; private final ContextPropagators propagators; static { @@ -77,7 +78,7 @@ private ReplaySafeOpenTelemetry(Builder builder) { .setIdGenerator(new ReplaySafeIdGenerator(builder.idGenerator)) .build()); this.meterProvider = new ReplaySafeMeterProvider(builder.meterProviderBuilder.build()); - this.loggerProvider = builder.loggerProviderBuilder.build(); + this.loggerProvider = new ReplaySafeLoggerProvider(builder.loggerProviderBuilder.build()); this.propagators = builder.propagators; } @@ -253,6 +254,54 @@ public Meter build() { } } + private static final class ReplaySafeLoggerProvider implements LoggerProvider, Closeable { + private final SdkLoggerProvider delegate; + + private ReplaySafeLoggerProvider(SdkLoggerProvider delegate) { + this.delegate = delegate; + } + + @Override + public Logger get(@Nonnull String instrumentationScopeName) { + return new ReplaySafeLogger(delegate.get(instrumentationScopeName)); + } + + @Override + public LoggerBuilder loggerBuilder(@Nonnull String instrumentationScopeName) { + return new ReplaySafeLoggerBuilder(delegate.loggerBuilder(instrumentationScopeName)); + } + + @Override + public void close() { + delegate.close(); + } + } + + private static final class ReplaySafeLoggerBuilder implements LoggerBuilder { + private final LoggerBuilder delegate; + + private ReplaySafeLoggerBuilder(LoggerBuilder delegate) { + this.delegate = delegate; + } + + @Override + public LoggerBuilder setSchemaUrl(@Nonnull String schemaUrl) { + delegate.setSchemaUrl(schemaUrl); + return this; + } + + @Override + public LoggerBuilder setInstrumentationVersion(@Nonnull String instrumentationScopeVersion) { + delegate.setInstrumentationVersion(instrumentationScopeVersion); + return this; + } + + @Override + public Logger build() { + return new ReplaySafeLogger(delegate.build()); + } + } + private static final class ReplaySafeTracerBuilder implements TracerBuilder { private final TracerBuilder delegate; private final String instrumentationScopeName; diff --git a/contrib/temporal-opentelemetry-v2/src/main/java/io/temporal/opentelemetry/v2/internal/ReplaySafeLogger.java b/contrib/temporal-opentelemetry-v2/src/main/java/io/temporal/opentelemetry/v2/internal/ReplaySafeLogger.java new file mode 100644 index 0000000000..68c8297b60 --- /dev/null +++ b/contrib/temporal-opentelemetry-v2/src/main/java/io/temporal/opentelemetry/v2/internal/ReplaySafeLogger.java @@ -0,0 +1,170 @@ +package io.temporal.opentelemetry.v2.internal; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.context.Context; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +/** + * Wraps a logger so the records it builds are not emitted by replaying workflow code, which would + * otherwise emit them again on every replay. Replay is checked on the thread that calls {@link + * LogRecordBuilder#emit()}, so records emitted on another thread, such as an asynchronous + * appender's worker, are not suppressed. + */ +public final class ReplaySafeLogger implements Logger { + private final Logger delegate; + + public ReplaySafeLogger(Logger delegate) { + this.delegate = delegate; + } + + @Override + public LogRecordBuilder logRecordBuilder() { + return new ReplaySafeLogRecordBuilder(delegate.logRecordBuilder()); + } + + @Override + public boolean isEnabled(Severity severity, Context context) { + return !OpenTelemetrySuppression.shouldSuppress() && delegate.isEnabled(severity, context); + } + + @Override + public boolean isEnabled(Severity severity) { + return !OpenTelemetrySuppression.shouldSuppress() && delegate.isEnabled(severity); + } + + private static final class ReplaySafeLogRecordBuilder implements LogRecordBuilder { + private final LogRecordBuilder delegate; + + ReplaySafeLogRecordBuilder(LogRecordBuilder delegate) { + this.delegate = delegate; + } + + @Override + public void emit() { + if (OpenTelemetrySuppression.shouldSuppress()) { + return; + } + delegate.emit(); + } + + @Override + public LogRecordBuilder setTimestamp(long timestamp, TimeUnit unit) { + delegate.setTimestamp(timestamp, unit); + return this; + } + + @Override + public LogRecordBuilder setTimestamp(Instant instant) { + delegate.setTimestamp(instant); + return this; + } + + @Override + public LogRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit) { + delegate.setObservedTimestamp(timestamp, unit); + return this; + } + + @Override + public LogRecordBuilder setObservedTimestamp(Instant instant) { + delegate.setObservedTimestamp(instant); + return this; + } + + @Override + public LogRecordBuilder setContext(Context context) { + delegate.setContext(context); + return this; + } + + @Override + public LogRecordBuilder setSeverity(Severity severity) { + delegate.setSeverity(severity); + return this; + } + + @Override + public LogRecordBuilder setSeverityText(String severityText) { + delegate.setSeverityText(severityText); + return this; + } + + @Override + public LogRecordBuilder setBody(String body) { + delegate.setBody(body); + return this; + } + + @Override + public LogRecordBuilder setBody(Value body) { + delegate.setBody(body); + return this; + } + + @Override + public LogRecordBuilder setAllAttributes(Attributes attributes) { + delegate.setAllAttributes(attributes); + return this; + } + + @Override + public LogRecordBuilder setAttribute(AttributeKey key, T value) { + delegate.setAttribute(key, value); + return this; + } + + @Override + public LogRecordBuilder setAttribute(String key, String value) { + delegate.setAttribute(key, value); + return this; + } + + @Override + public LogRecordBuilder setAttribute(String key, long value) { + delegate.setAttribute(key, value); + return this; + } + + @Override + public LogRecordBuilder setAttribute(String key, double value) { + delegate.setAttribute(key, value); + return this; + } + + @Override + public LogRecordBuilder setAttribute(String key, boolean value) { + delegate.setAttribute(key, value); + return this; + } + + @Override + public LogRecordBuilder setAttribute(String key, int value) { + delegate.setAttribute(key, value); + return this; + } + + @Override + public LogRecordBuilder setAttribute(String key, Value value) { + delegate.setAttribute(key, value); + return this; + } + + @Override + public LogRecordBuilder setEventName(String eventName) { + delegate.setEventName(eventName); + return this; + } + + @Override + public LogRecordBuilder setException(Throwable throwable) { + delegate.setException(throwable); + return this; + } + } +} diff --git a/contrib/temporal-opentelemetry-v2/src/test/java/io/temporal/opentelemetry/v2/LoggerTest.java b/contrib/temporal-opentelemetry-v2/src/test/java/io/temporal/opentelemetry/v2/LoggerTest.java new file mode 100644 index 0000000000..24e6992613 --- /dev/null +++ b/contrib/temporal-opentelemetry-v2/src/test/java/io/temporal/opentelemetry/v2/LoggerTest.java @@ -0,0 +1,116 @@ +package io.temporal.opentelemetry.v2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowStub; +import io.temporal.testing.WorkflowReplayer; +import io.temporal.testing.internal.SDKTestWorkflowRule; +import io.temporal.workflow.QueryMethod; +import io.temporal.workflow.SignalMethod; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; +import java.util.ArrayList; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Verifies that log records emit during live execution and that replaying a workflow run does not + * emit duplicate records. + */ +public class LoggerTest extends OtelTestBase { + private static final Logger logger = LoggerFactory.getLogger(LoggerTest.class); + private static final String RUN_BODY = "custom workflow run"; + private static final String QUERY_BODY = "custom workflow query"; + + @WorkflowInterface + public interface TestWorkflow { + @WorkflowMethod + void run(); + + @QueryMethod + String queryAndEmit(); + + @SignalMethod + void proceed(); + } + + public static class TestWorkflowImpl implements TestWorkflow { + private boolean proceed; + + @Override + public void run() { + emit(RUN_BODY); + Workflow.await(() -> proceed); + } + + @Override + public String queryAndEmit() { + emit(QUERY_BODY); + return "ok"; + } + + @Override + public void proceed() { + proceed = true; + } + + private static void emit(String body) { + logger.info(body); + } + } + + @Rule + public SDKTestWorkflowRule testWorkflowRule = + newRuleBuilder(false).setWorkflowTypes(TestWorkflowImpl.class).build(); + + @Test + public void recordsEmitOutsideWorkflows() { + logger.warn("outside"); + + List logs = emittedLogs(); + assertEquals(1, logs.size()); + LogRecordData log = logs.get(0); + assertEquals(Severity.WARN, log.getSeverity()); + assertEquals("outside", log.getBodyValue().asString()); + } + + /** + * The live run and the live query handler each emit once; replaying the finished run drops its + * record. + */ + @Test + public void liveExecutionEmitsAndReplayDoesNotDuplicate() throws Exception { + TestWorkflow workflow = testWorkflowRule.newWorkflowStub(TestWorkflow.class); + WorkflowExecution execution = WorkflowClient.start(workflow::run); + + assertEquals("ok", workflow.queryAndEmit()); + workflow.proceed(); + WorkflowStub.fromTyped(workflow).getResult(Void.class); + + // Replay the full workflow history and verify the run's record is suppressed on replay. + WorkflowReplayer.replayWorkflowExecution( + testWorkflowRule.getExecutionHistory(execution.getWorkflowId()), TestWorkflowImpl.class); + + List bodies = emittedBodies(); + assertEquals(bodies.toString(), 2, bodies.size()); + assertTrue(bodies.toString(), bodies.contains(RUN_BODY)); + assertTrue(bodies.toString(), bodies.contains(QUERY_BODY)); + } + + private static List emittedBodies() { + List bodies = new ArrayList<>(); + for (LogRecordData log : emittedLogs()) { + bodies.add(log.getBodyValue().asString()); + } + return bodies; + } +} diff --git a/contrib/temporal-opentelemetry-v2/src/test/java/io/temporal/opentelemetry/v2/OtelTestBase.java b/contrib/temporal-opentelemetry-v2/src/test/java/io/temporal/opentelemetry/v2/OtelTestBase.java index 8c826ff0b6..48e10720bf 100644 --- a/contrib/temporal-opentelemetry-v2/src/test/java/io/temporal/opentelemetry/v2/OtelTestBase.java +++ b/contrib/temporal-opentelemetry-v2/src/test/java/io/temporal/opentelemetry/v2/OtelTestBase.java @@ -6,8 +6,13 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.SdkTracerProvider; @@ -26,6 +31,7 @@ public abstract class OtelTestBase { static final InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); static final InMemoryMetricReader metricReader = InMemoryMetricReader.create(); + static final InMemoryLogRecordExporter logExporter = InMemoryLogRecordExporter.create(); private static ReplaySafeOpenTelemetry openTelemetry; @BeforeClass @@ -36,8 +42,12 @@ public static void registerGlobalOpenTelemetry() { SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(spanExporter))) .setMeterProviderBuilder(SdkMeterProvider.builder().registerMetricReader(metricReader)) + .setLoggerProviderBuilder( + SdkLoggerProvider.builder() + .addLogRecordProcessor(SimpleLogRecordProcessor.create(logExporter))) .build(); GlobalOpenTelemetry.set(openTelemetry); + OpenTelemetryAppender.install(openTelemetry); } @AfterClass @@ -47,8 +57,9 @@ public static void resetGlobalOpenTelemetry() { } @Before - public void clearSpans() { + public void clearSpansAndLogs() { spanExporter.reset(); + logExporter.reset(); } /** @@ -67,6 +78,10 @@ static List endedSpans() { return spanExporter.getFinishedSpanItems(); } + static List emittedLogs() { + return logExporter.getFinishedLogRecordItems(); + } + static SpanData requireSpanNamed(List spans, String name) { for (SpanData span : spans) { if (span.getName().equals(name)) { diff --git a/contrib/temporal-opentelemetry-v2/src/test/resources/logback-test.xml b/contrib/temporal-opentelemetry-v2/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..4dc91ceb52 --- /dev/null +++ b/contrib/temporal-opentelemetry-v2/src/test/resources/logback-test.xml @@ -0,0 +1,17 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + +