diff --git a/profiler/build.gradle.kts b/profiler/build.gradle.kts index 8db13e10e..e6ea278fd 100644 --- a/profiler/build.gradle.kts +++ b/profiler/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { annotationProcessor("com.google.auto.service:auto-service") compileOnly("com.google.auto.service:auto-service") + testImplementation(project(":custom")) testImplementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") testImplementation("io.grpc:grpc-netty") testImplementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/Configuration.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/Configuration.java index 9276fe91d..6780b3a30 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/Configuration.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/Configuration.java @@ -74,6 +74,12 @@ public class Configuration implements AutoConfigurationCustomizerProvider { public static final String CONFIG_KEY_SNAPSHOT_SELECTION_RATE = "splunk.snapshot.selection.rate"; private static final double DEFAULT_SNAPSHOT_SELECTION_RATE = 0.01; private static final double MAX_SNAPSHOT_SELECTION_RATE = 0.10; + private static final String CONFIG_KEY_SNAPSHOT_PROFILER_STACK_DEPTH = + "splunk.snapshot.profiler.max.stack.depth"; + private static final int DEFAULT_SNAPSHOT_PROFILER_STACK_DEPTH = 1024; + private static final String CONFIG_KEY_SNAPSHOT_PROFILER_SAMPLING_INTERVAL = + "splunk.snapshot.profiler.sampling.interval"; + public static final Duration DEFAULT_SNAPSHOT_PROFILER_SAMPLING_INTERVAL = Duration.ofMillis(20); @Override public void customize(AutoConfigurationCustomizer autoConfiguration) { @@ -181,6 +187,10 @@ private static int getJavaVersion() { return Integer.parseInt(javaSpecVersion); } + public static boolean isSnapshotProfilingEnabled(ConfigProperties properties) { + return properties.getBoolean(CONFIG_KEY_ENABLE_SNAPSHOT_PROFILER, false); + } + public static double getSnapshotSelectionRate(ConfigProperties properties) { String selectionRatePropertyValue = properties.getString( @@ -207,4 +217,15 @@ public static double getSnapshotSelectionRate(ConfigProperties properties) { return DEFAULT_SNAPSHOT_SELECTION_RATE; } } + + public static int getSnapshotProfilerStackDepth(ConfigProperties properties) { + return properties.getInt( + CONFIG_KEY_SNAPSHOT_PROFILER_STACK_DEPTH, DEFAULT_SNAPSHOT_PROFILER_STACK_DEPTH); + } + + public static Duration getSnapshotProfilerSamplingInterval(ConfigProperties properties) { + return properties.getDuration( + CONFIG_KEY_SNAPSHOT_PROFILER_SAMPLING_INTERVAL, + DEFAULT_SNAPSHOT_PROFILER_SAMPLING_INTERVAL); + } } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java index 906eedeb4..8a93d9702 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/JfrActivator.java @@ -29,7 +29,6 @@ import com.splunk.opentelemetry.profiler.allocation.exporter.AllocationEventExporter; import com.splunk.opentelemetry.profiler.allocation.exporter.PprofAllocationEventExporter; import com.splunk.opentelemetry.profiler.context.SpanContextualizer; -import com.splunk.opentelemetry.profiler.events.EventPeriods; import com.splunk.opentelemetry.profiler.exporter.CpuEventExporter; import com.splunk.opentelemetry.profiler.exporter.PprofCpuEventExporter; import com.splunk.opentelemetry.profiler.util.HelpfulExecutors; @@ -130,13 +129,12 @@ private void activateJfrAndRunForever(ConfigProperties config, Resource resource EventReader eventReader = new EventReader(); SpanContextualizer spanContextualizer = new SpanContextualizer(eventReader); - EventPeriods periods = new EventPeriods(jfrSettings::get); LogRecordExporter logsExporter = LogExporterBuilder.fromConfig(config); CpuEventExporter cpuEventExporter = PprofCpuEventExporter.builder() .otelLogger(buildOtelLogger(SimpleLogRecordProcessor.create(logsExporter), resource)) - .eventPeriods(periods) + .period(Configuration.getCallStackInterval(config)) .stackDepth(stackDepth) .build(); diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/OtelLoggerFactory.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/OtelLoggerFactory.java new file mode 100644 index 000000000..8deb5178b --- /dev/null +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/OtelLoggerFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler; + +import com.google.common.annotations.VisibleForTesting; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.logs.LogRecordProcessor; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.resources.Resource; +import java.util.function.Function; + +public class OtelLoggerFactory { + private final Function logRecordExporter; + + public OtelLoggerFactory() { + this(LogExporterBuilder::fromConfig); + } + + @VisibleForTesting + public OtelLoggerFactory(Function logRecordExporter) { + this.logRecordExporter = logRecordExporter; + } + + public Logger build(ConfigProperties properties, Resource resource) { + LogRecordExporter exporter = createLogRecordExporter(properties); + LogRecordProcessor processor = SimpleLogRecordProcessor.create(exporter); + return buildOtelLogger(processor, resource); + } + + private LogRecordExporter createLogRecordExporter(ConfigProperties properties) { + return logRecordExporter.apply(properties); + } + + private Logger buildOtelLogger(LogRecordProcessor logProcessor, Resource resource) { + return SdkLoggerProvider.builder() + .addLogRecordProcessor(logProcessor) + .setResource(resource) + .build() + .loggerBuilder(ProfilingSemanticAttributes.OTEL_INSTRUMENTATION_NAME) + .setInstrumentationVersion(ProfilingSemanticAttributes.OTEL_INSTRUMENTATION_VERSION) + .build(); + } +} diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/events/EventPeriods.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/events/EventPeriods.java deleted file mode 100644 index ded9b170a..000000000 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/events/EventPeriods.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.splunk.opentelemetry.profiler.events; - -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -public class EventPeriods { - - public static final Duration UNKNOWN = Duration.ZERO; - private final Map cache = new HashMap<>(); - private final Function configFinder; - - public EventPeriods(Function configFinder) { - this.configFinder = configFinder; - } - - public Duration getDuration(String eventName) { - return cache.computeIfAbsent( - eventName, - event -> { - String value = configFinder.apply(event + "#period"); - return parseToDuration(value); - }); - } - - private Duration parseToDuration(String value) { - if (value == null) { - return UNKNOWN; - } - // format is "TTT UUU" where TTT is some numbers and UUU is some units suffix (ms or s) - try { - String[] parts = value.split(" "); - if (parts.length < 2) { - return UNKNOWN; - } - long multiplier = 1; - if ("s".equals(parts[1])) { - multiplier = 1000; - } - return Duration.ofMillis(multiplier * Integer.parseInt(parts[0])); - } catch (NumberFormatException e) { - return UNKNOWN; - } - } -} diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/exporter/CpuEventExporter.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/exporter/CpuEventExporter.java index 2c239b692..ac6499c97 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/exporter/CpuEventExporter.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/exporter/CpuEventExporter.java @@ -17,10 +17,20 @@ package com.splunk.opentelemetry.profiler.exporter; import com.splunk.opentelemetry.profiler.context.StackToSpanLinkage; +import java.time.Instant; public interface CpuEventExporter { void export(StackToSpanLinkage stackToSpanLinkage); + default void export( + long threadId, + String threadName, + Thread.State threadState, + StackTraceElement[] stackTrace, + Instant eventTime, + String traceId, + String spanId) {} + default void flush() {} } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/exporter/PprofCpuEventExporter.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/exporter/PprofCpuEventExporter.java index 4996c5b15..0af068360 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/exporter/PprofCpuEventExporter.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/exporter/PprofCpuEventExporter.java @@ -30,26 +30,27 @@ import com.splunk.opentelemetry.profiler.InstrumentationSource; import com.splunk.opentelemetry.profiler.ProfilingDataType; import com.splunk.opentelemetry.profiler.context.StackToSpanLinkage; -import com.splunk.opentelemetry.profiler.events.EventPeriods; import com.splunk.opentelemetry.profiler.exporter.StackTraceParser.StackTrace; import com.splunk.opentelemetry.profiler.pprof.Pprof; import io.opentelemetry.api.logs.Logger; import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanId; +import io.opentelemetry.api.trace.TraceId; import java.time.Duration; import java.time.Instant; public class PprofCpuEventExporter implements CpuEventExporter { - private final EventPeriods eventPeriods; + private final Duration period; private final int stackDepth; private final PprofLogDataExporter pprofLogDataExporter; private Pprof pprof = createPprof(); private PprofCpuEventExporter(Builder builder) { - this.eventPeriods = builder.eventPeriods; + this.period = builder.period; this.stackDepth = builder.stackDepth; this.pprofLogDataExporter = new PprofLogDataExporter( - builder.otelLogger, ProfilingDataType.CPU, InstrumentationSource.CONTINUOUS); + builder.otelLogger, ProfilingDataType.CPU, builder.instrumentationSource); } @Override @@ -80,10 +81,7 @@ public void export(StackToSpanLinkage stackToSpanLinkage) { String eventName = stackToSpanLinkage.getSourceEventName(); pprof.addLabel(sample, SOURCE_EVENT_NAME, eventName); - Duration eventPeriod = eventPeriods.getDuration(eventName); - if (!EventPeriods.UNKNOWN.equals(eventPeriod)) { - pprof.addLabel(sample, SOURCE_EVENT_PERIOD, eventPeriod.toMillis()); - } + pprof.addLabel(sample, SOURCE_EVENT_PERIOD, period.toMillis()); Instant time = stackToSpanLinkage.getTime(); pprof.addLabel(sample, SOURCE_EVENT_TIME, time.toEpochMilli()); @@ -96,6 +94,52 @@ public void export(StackToSpanLinkage stackToSpanLinkage) { pprof.getProfileBuilder().addSample(sample); } + @Override + public void export( + long threadId, + String threadName, + Thread.State threadState, + StackTraceElement[] stackTrace, + Instant eventTime, + String traceId, + String spanId) { + Sample.Builder sample = Sample.newBuilder(); + + pprof.addLabel(sample, THREAD_ID, threadId); + pprof.addLabel(sample, THREAD_NAME, threadName); + pprof.addLabel(sample, THREAD_STATE, threadState.name()); + + if (stackTrace.length > stackDepth) { + pprof.addLabel(sample, THREAD_STACK_TRUNCATED, true); + } + + for (int i = 0; i < Math.min(stackDepth, stackTrace.length); i++) { + StackTraceElement ste = stackTrace[i]; + + String fileName = ste.getFileName(); + if (fileName == null) { + fileName = "unknown"; + } + String className = ste.getClassName(); + String methodName = ste.getMethodName(); + int lineNumber = Math.max(ste.getLineNumber(), 0); + sample.addLocationId(pprof.getLocationId(fileName, className, methodName, lineNumber)); + pprof.incFrameCount(); + } + + pprof.addLabel(sample, SOURCE_EVENT_PERIOD, period.toMillis()); + pprof.addLabel(sample, SOURCE_EVENT_TIME, eventTime.toEpochMilli()); + + if (TraceId.isValid(traceId)) { + pprof.addLabel(sample, TRACE_ID, traceId); + } + if (SpanId.isValid(spanId)) { + pprof.addLabel(sample, SPAN_ID, spanId); + } + + pprof.getProfileBuilder().addSample(sample); + } + private static Pprof createPprof() { return new Pprof(); } @@ -123,8 +167,9 @@ public static Builder builder() { public static class Builder { private Logger otelLogger; - private EventPeriods eventPeriods; + private Duration period; private int stackDepth; + private InstrumentationSource instrumentationSource = InstrumentationSource.CONTINUOUS; public PprofCpuEventExporter build() { return new PprofCpuEventExporter(this); @@ -135,8 +180,8 @@ public Builder otelLogger(Logger otelLogger) { return this; } - public Builder eventPeriods(EventPeriods eventPeriods) { - this.eventPeriods = eventPeriods; + public Builder period(Duration period) { + this.period = period; return this; } @@ -144,5 +189,10 @@ public Builder stackDepth(int stackDepth) { this.stackDepth = stackDepth; return this; } + + public Builder instrumentationSource(InstrumentationSource instrumentationSource) { + this.instrumentationSource = instrumentationSource; + return this; + } } } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/AccumulatingStagingArea.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/AccumulatingStagingArea.java new file mode 100644 index 000000000..3af6f4778 --- /dev/null +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/AccumulatingStagingArea.java @@ -0,0 +1,53 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Supplier; + +class AccumulatingStagingArea implements StagingArea { + private final ConcurrentMap> stackTraces = new ConcurrentHashMap<>(); + private final Supplier exporter; + + AccumulatingStagingArea(Supplier exporter) { + this.exporter = exporter; + } + + @Override + public void stage(String traceId, StackTrace stackTrace) { + stackTraces.compute( + traceId, + (id, stackTraces) -> { + if (stackTraces == null) { + stackTraces = new ArrayList<>(); + } + stackTraces.add(stackTrace); + return stackTraces; + }); + } + + @Override + public void empty(String traceId) { + List stackTraces = this.stackTraces.remove(traceId); + if (stackTraces != null) { + exporter.get().export(stackTraces); + } + } +} diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/AsyncStackTraceExporter.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/AsyncStackTraceExporter.java new file mode 100644 index 000000000..3fb36c8ae --- /dev/null +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/AsyncStackTraceExporter.java @@ -0,0 +1,76 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import com.splunk.opentelemetry.profiler.InstrumentationSource; +import com.splunk.opentelemetry.profiler.exporter.CpuEventExporter; +import com.splunk.opentelemetry.profiler.exporter.PprofCpuEventExporter; +import io.opentelemetry.api.logs.Logger; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; + +class AsyncStackTraceExporter implements StackTraceExporter { + private static final java.util.logging.Logger logger = + java.util.logging.Logger.getLogger(AsyncStackTraceExporter.class.getName()); + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final Logger otelLogger; + private final Duration samplingPeriod; + private final int maxDepth; + + AsyncStackTraceExporter(Logger logger, Duration samplingPeriod, int maxDepth) { + this.otelLogger = logger; + this.samplingPeriod = samplingPeriod; + this.maxDepth = maxDepth; + } + + @Override + public void export(List stackTraces) { + executor.submit(pprofExporter(otelLogger, stackTraces)); + } + + private Runnable pprofExporter(Logger otelLogger, List stackTraces) { + return () -> { + try { + CpuEventExporter cpuEventExporter = + PprofCpuEventExporter.builder() + .otelLogger(otelLogger) + .stackDepth(maxDepth) + .period(samplingPeriod) + .instrumentationSource(InstrumentationSource.SNAPSHOT) + .build(); + + for (StackTrace stackTrace : stackTraces) { + cpuEventExporter.export( + stackTrace.getThreadId(), + stackTrace.getThreadName(), + stackTrace.getThreadState(), + stackTrace.getStackFrames(), + stackTrace.getTimestamp(), + stackTrace.getTraceId(), + null); + } + cpuEventExporter.flush(); + } catch (Exception e) { + logger.log(Level.SEVERE, "An exception was thrown while exporting profiling snapshots.", e); + } + }; + } +} diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/ScheduledExecutorStackTraceSampler.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/ScheduledExecutorStackTraceSampler.java index c9345c5af..1d8a4669d 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/ScheduledExecutorStackTraceSampler.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/ScheduledExecutorStackTraceSampler.java @@ -16,7 +16,6 @@ package com.splunk.opentelemetry.profiler.snapshot; -import com.google.common.annotations.VisibleForTesting; import io.opentelemetry.api.trace.SpanContext; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; @@ -36,8 +35,6 @@ class ScheduledExecutorStackTraceSampler implements StackTraceSampler { private static final Logger logger = Logger.getLogger(ScheduledExecutorStackTraceSampler.class.getName()); private static final int SCHEDULER_INITIAL_DELAY = 0; - private static final Duration SCHEDULER_PERIOD = Duration.ofMillis(20); - private static final int MAX_ENTRY_DEPTH = 200; private final ConcurrentMap samplers = new ConcurrentHashMap<>(); @@ -45,11 +42,6 @@ class ScheduledExecutorStackTraceSampler implements StackTraceSampler { private final StagingArea stagingArea; private final Duration samplingPeriod; - ScheduledExecutorStackTraceSampler(StagingArea stagingArea) { - this(stagingArea, SCHEDULER_PERIOD); - } - - @VisibleForTesting ScheduledExecutorStackTraceSampler(StagingArea stagingArea, Duration samplingPeriod) { this.stagingArea = stagingArea; this.samplingPeriod = samplingPeriod; @@ -62,7 +54,8 @@ public void start(SpanContext spanContext) { traceId -> { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate( - new StackTraceGatherer(traceId, Thread.currentThread().getId()), + new StackTraceGatherer( + samplingPeriod, spanContext.getTraceId(), Thread.currentThread().getId()), SCHEDULER_INITIAL_DELAY, samplingPeriod.toMillis(), TimeUnit.MILLISECONDS); @@ -80,20 +73,22 @@ public void stop(SpanContext spanContext) { } class StackTraceGatherer implements Runnable { + private final Duration samplingPeriod; private final String traceId; private final long threadId; - StackTraceGatherer(String traceId, long threadId) { + StackTraceGatherer(Duration samplingPeriod, String traceId, long threadId) { + this.samplingPeriod = samplingPeriod; this.traceId = traceId; this.threadId = threadId; } @Override public void run() { - Instant now = Instant.now(); try { - ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId, MAX_ENTRY_DEPTH); - StackTrace stackTrace = StackTrace.from(now, threadInfo); + Instant now = Instant.now(); + ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId, Integer.MAX_VALUE); + StackTrace stackTrace = StackTrace.from(now, samplingPeriod, traceId, threadInfo); stagingArea.stage(traceId, stackTrace); } catch (Exception e) { logger.log(Level.SEVERE, e, samplerErrorMessage(traceId, threadId)); diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingSdkCustomizer.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingSdkCustomizer.java index e6e7f5da8..fc035b4c5 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingSdkCustomizer.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingSdkCustomizer.java @@ -16,14 +16,14 @@ package com.splunk.opentelemetry.profiler.snapshot; -import static com.splunk.opentelemetry.profiler.Configuration.CONFIG_KEY_ENABLE_SNAPSHOT_PROFILER; - import com.google.auto.service.AutoService; import com.google.common.annotations.VisibleForTesting; +import com.splunk.opentelemetry.profiler.Configuration; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import java.time.Duration; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Map; @@ -34,16 +34,29 @@ @AutoService(AutoConfigurationCustomizerProvider.class) public class SnapshotProfilingSdkCustomizer implements AutoConfigurationCustomizerProvider { private final TraceRegistry registry; - private final StackTraceSampler sampler; + private final Function samplerProvider; public SnapshotProfilingSdkCustomizer() { - this(new TraceRegistry(), new ScheduledExecutorStackTraceSampler(new NoopStagingArea())); + this(new TraceRegistry(), stackTraceSamplerProvider()); + } + + private static Function stackTraceSamplerProvider() { + return properties -> { + Duration samplingPeriod = Configuration.getSnapshotProfilerSamplingInterval(properties); + return new ScheduledExecutorStackTraceSampler( + new AccumulatingStagingArea(StackTraceExporterProvider.INSTANCE), samplingPeriod); + }; } @VisibleForTesting - SnapshotProfilingSdkCustomizer(TraceRegistry registry, StackTraceSampler sampler) { + SnapshotProfilingSdkCustomizer(TraceRegistry registry, StackTraceSampler samplerProvider) { + this(registry, properties -> samplerProvider); + } + + private SnapshotProfilingSdkCustomizer( + TraceRegistry registry, Function samplerProvider) { this.registry = registry; - this.sampler = sampler; + this.samplerProvider = samplerProvider; } @Override @@ -57,6 +70,7 @@ public void customize(AutoConfigurationCustomizer autoConfigurationCustomizer) { snapshotProfilingSpanProcessor(TraceRegistry registry) { return (builder, properties) -> { if (snapshotProfilingEnabled(properties)) { + StackTraceSampler sampler = samplerProvider.apply(properties); return builder.addSpanProcessor(new SnapshotProfilingSpanProcessor(registry, sampler)); } return builder; @@ -97,7 +111,7 @@ private boolean includeTraceContextPropagator(Set configuredPropagators) return configuredPropagators.isEmpty(); } - private boolean snapshotProfilingEnabled(ConfigProperties config) { - return config.getBoolean(CONFIG_KEY_ENABLE_SNAPSHOT_PROFILER, false); + private boolean snapshotProfilingEnabled(ConfigProperties properties) { + return Configuration.isSnapshotProfilingEnabled(properties); } } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTrace.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTrace.java index 32a868266..eab765901 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTrace.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTrace.java @@ -16,25 +16,46 @@ package com.splunk.opentelemetry.profiler.snapshot; +import com.google.common.annotations.VisibleForTesting; import java.lang.management.ThreadInfo; +import java.time.Duration; import java.time.Instant; class StackTrace { - static StackTrace from(Instant timestamp, ThreadInfo thread) { + static StackTrace from(Instant timestamp, Duration duration, String traceId, ThreadInfo thread) { return new StackTrace( - timestamp, thread.getThreadId(), thread.getThreadName(), thread.getStackTrace()); + timestamp, + duration, + traceId, + thread.getThreadId(), + thread.getThreadName(), + thread.getThreadState(), + thread.getStackTrace()); } private final Instant timestamp; + private final Duration duration; + private final String traceId; private final long threadId; private final String threadName; + private final Thread.State threadState; private final StackTraceElement[] stackFrames; - private StackTrace( - Instant timestamp, long threadId, String threadName, StackTraceElement[] stackFrames) { + @VisibleForTesting + StackTrace( + Instant timestamp, + Duration duration, + String traceId, + long threadId, + String threadName, + Thread.State threadState, + StackTraceElement[] stackFrames) { this.timestamp = timestamp; + this.duration = duration; + this.traceId = traceId; this.threadId = threadId; this.threadName = threadName; + this.threadState = threadState; this.stackFrames = stackFrames; } @@ -42,6 +63,14 @@ Instant getTimestamp() { return timestamp; } + Duration getDuration() { + return duration; + } + + String getTraceId() { + return traceId; + } + long getThreadId() { return threadId; } @@ -50,6 +79,10 @@ String getThreadName() { return threadName; } + Thread.State getThreadState() { + return threadState; + } + StackTraceElement[] getStackFrames() { return stackFrames; } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/NoopStagingArea.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporter.java similarity index 72% rename from profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/NoopStagingArea.java rename to profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporter.java index d9455d9ac..e583ba3ad 100644 --- a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/NoopStagingArea.java +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporter.java @@ -16,10 +16,11 @@ package com.splunk.opentelemetry.profiler.snapshot; -class NoopStagingArea implements StagingArea { - @Override - public void stage(String traceId, StackTrace stackTrace) {} +import java.util.List; - @Override - public void empty(String traceId) {} +/** Works in concert with the {@link StagingArea} to export a batch of {@link StackTrace}s */ +interface StackTraceExporter { + StackTraceExporter NOOP = stackTraces -> {}; + + void export(List stackTraces); } diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterActivator.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterActivator.java new file mode 100644 index 000000000..c6d2d90e7 --- /dev/null +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterActivator.java @@ -0,0 +1,65 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import com.google.auto.service.AutoService; +import com.google.common.annotations.VisibleForTesting; +import com.splunk.opentelemetry.profiler.Configuration; +import com.splunk.opentelemetry.profiler.OtelLoggerFactory; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.javaagent.extension.AgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import java.time.Duration; + +@AutoService(AgentListener.class) +public class StackTraceExporterActivator implements AgentListener { + private final OtelLoggerFactory otelLoggerFactory; + + public StackTraceExporterActivator() { + this(new OtelLoggerFactory()); + } + + @VisibleForTesting + StackTraceExporterActivator(OtelLoggerFactory otelLoggerFactory) { + this.otelLoggerFactory = otelLoggerFactory; + } + + @Override + public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + ConfigProperties properties = AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk); + if (snapshotProfilingEnabled(properties)) { + int maxDepth = Configuration.getSnapshotProfilerStackDepth(properties); + Duration samplingPeriod = Configuration.getSnapshotProfilerSamplingInterval(properties); + Logger logger = buildLogger(autoConfiguredOpenTelemetrySdk, properties); + AsyncStackTraceExporter exporter = + new AsyncStackTraceExporter(logger, samplingPeriod, maxDepth); + StackTraceExporterProvider.INSTANCE.configure(exporter); + } + } + + private boolean snapshotProfilingEnabled(ConfigProperties properties) { + return Configuration.isSnapshotProfilingEnabled(properties); + } + + private Logger buildLogger(AutoConfiguredOpenTelemetrySdk sdk, ConfigProperties properties) { + Resource resource = AutoConfigureUtil.getResource(sdk); + return otelLoggerFactory.build(properties, resource); + } +} diff --git a/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterProvider.java b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterProvider.java new file mode 100644 index 000000000..72c0320a3 --- /dev/null +++ b/profiler/src/main/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import com.google.common.annotations.VisibleForTesting; +import java.util.function.Supplier; + +class StackTraceExporterProvider implements Supplier { + public static final StackTraceExporterProvider INSTANCE = new StackTraceExporterProvider(); + + private StackTraceExporter exporter; + + @Override + public StackTraceExporter get() { + if (exporter == null) { + return StackTraceExporter.NOOP; + } + return exporter; + } + + void configure(StackTraceExporter exporter) { + this.exporter = exporter; + } + + @VisibleForTesting + void reset() { + exporter = null; + } + + private StackTraceExporterProvider() {} +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ConfigurationTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ConfigurationTest.java index 672d9d1c4..5ecc3e112 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/ConfigurationTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/ConfigurationTest.java @@ -17,12 +17,14 @@ package com.splunk.opentelemetry.profiler; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -106,6 +108,21 @@ void snapshotProfilingDisabledByDefault() { assertEquals("false", properties.get("splunk.snapshot.profiler.enabled")); } + @Test + void isSnapshotProfilingEnabledIsFalseByDefault() { + var properties = DefaultConfigProperties.create(Collections.emptyMap()); + assertFalse(Configuration.isSnapshotProfilingEnabled(properties)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void isSnapshotProfilingEnabled(boolean enabled) { + var properties = + DefaultConfigProperties.create( + Map.of("splunk.snapshot.profiler.enabled", String.valueOf(enabled))); + assertEquals(enabled, Configuration.isSnapshotProfilingEnabled(properties)); + } + @Test void snapshotSelectionDefaultRate() { Configuration configuration = new Configuration(); @@ -154,4 +171,37 @@ void getSnapshotSelectionRateUsesMaxSelectionRateWhenConfiguredRateIsHigher( double actualSelectionRate = Configuration.getSnapshotSelectionRate(properties); assertEquals(0.10, actualSelectionRate); } + + @ParameterizedTest + @ValueSource(ints = {128, 512, 2056}) + void getConfiguredSnapshotProfilerStackDepth(int depth) { + var properties = + DefaultConfigProperties.create( + Map.of("splunk.snapshot.profiler.max.stack.depth", String.valueOf(depth))); + assertEquals(depth, Configuration.getSnapshotProfilerStackDepth(properties)); + } + + @Test + void getDefaultSnapshotProfilerStackDepthWhenNotSpecified() { + var properties = DefaultConfigProperties.create(Collections.emptyMap()); + assertEquals(1024, Configuration.getSnapshotProfilerStackDepth(properties)); + } + + @ParameterizedTest + @ValueSource(ints = {128, 512, 2056}) + void getConfiguredSnapshotProfilerSamplingInterval(int milliseconds) { + var properties = + DefaultConfigProperties.create( + Map.of("splunk.snapshot.profiler.sampling.interval", String.valueOf(milliseconds))); + assertEquals( + Duration.ofMillis(milliseconds), + Configuration.getSnapshotProfilerSamplingInterval(properties)); + } + + @Test + void getDefaultSnapshotProfilerSamplingInterval() { + var properties = DefaultConfigProperties.create(Collections.emptyMap()); + assertEquals( + Duration.ofMillis(20), Configuration.getSnapshotProfilerSamplingInterval(properties)); + } } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/OtelLoggerFactoryTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/OtelLoggerFactoryTest.java new file mode 100644 index 000000000..309d006f4 --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/OtelLoggerFactoryTest.java @@ -0,0 +1,68 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class OtelLoggerFactoryTest { + private final InMemoryLogRecordExporter exporter = InMemoryLogRecordExporter.create(); + private final OtelLoggerFactory factory = new OtelLoggerFactory(properties -> exporter); + + @Test + void configureLoggerWithProfilingInstrumentationScopeName() { + var properties = DefaultConfigProperties.create(Collections.emptyMap()); + var resource = Resource.getDefault(); + + var logger = factory.build(properties, resource); + logger.logRecordBuilder().setBody("test").emit(); + + var logRecord = exporter.getFinishedLogRecordItems().get(0); + assertEquals("otel.profiling", logRecord.getInstrumentationScopeInfo().getName()); + } + + @Test + void configureLoggerWithProfilingInstrumentationVersion() { + var properties = DefaultConfigProperties.create(Collections.emptyMap()); + var resource = Resource.getDefault(); + + var logger = factory.build(properties, resource); + logger.logRecordBuilder().setBody("test").emit(); + + var logRecord = exporter.getFinishedLogRecordItems().get(0); + assertEquals("0.1.0", logRecord.getInstrumentationScopeInfo().getVersion()); + } + + @Test + void configureLoggerWithOpenTelemetryResource() { + var properties = DefaultConfigProperties.create(Collections.emptyMap()); + var resource = Resource.create(Attributes.of(AttributeKey.stringKey("test"), "value")); + + var logger = factory.build(properties, resource); + logger.logRecordBuilder().setBody("test").emit(); + + var logRecord = exporter.getFinishedLogRecordItems().get(0); + assertEquals(resource, logRecord.getResource()); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/events/EventPeriodsTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/events/EventPeriodsTest.java deleted file mode 100644 index fa1284405..000000000 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/events/EventPeriodsTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.splunk.opentelemetry.profiler.events; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.when; - -import java.time.Duration; -import java.util.function.Function; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class EventPeriodsTest { - - @Mock Function configFinder; - - @Test - void testNotCachedFirstParse() { - EventPeriods eventPeriods = new EventPeriods(configFinder); - when(configFinder.apply("jdk.SomeEvent#period")).thenReturn("250 ms"); - Duration result = eventPeriods.getDuration("jdk.SomeEvent"); - assertEquals(Duration.ofMillis(250), result); - } - - @Test - void testCached() { - EventPeriods eventPeriods = new EventPeriods(configFinder); - when(configFinder.apply("jdk.SomeEvent#period")) - .thenReturn("26 s") - .thenThrow(new IllegalStateException()); - Duration result1 = eventPeriods.getDuration("jdk.SomeEvent"); - Duration result2 = eventPeriods.getDuration("jdk.SomeEvent"); - Duration result3 = eventPeriods.getDuration("jdk.SomeEvent"); - assertEquals(Duration.ofSeconds(26), result1); - assertEquals(Duration.ofSeconds(26), result2); - assertEquals(Duration.ofSeconds(26), result3); - } - - @Test - void testNotFoundAlsoCached() { - EventPeriods eventPeriods = new EventPeriods(configFinder); - when(configFinder.apply("jdk.SomeEvent#period")) - .thenReturn(null) - .thenThrow(new IllegalStateException()); - Duration result1 = eventPeriods.getDuration("jdk.SomeEvent"); - Duration result2 = eventPeriods.getDuration("jdk.SomeEvent"); - assertEquals(EventPeriods.UNKNOWN, result1); - assertEquals(EventPeriods.UNKNOWN, result2); - } - - @Test - void testNotParsedAlsoCached() { - EventPeriods eventPeriods = new EventPeriods(configFinder); - when(configFinder.apply("jdk.SomeEvent#period")) - .thenReturn("BLEAK BLOOP") - .thenThrow(new IllegalStateException()); - Duration result1 = eventPeriods.getDuration("jdk.SomeEvent"); - Duration result2 = eventPeriods.getDuration("jdk.SomeEvent"); - assertEquals(EventPeriods.UNKNOWN, result1); - assertEquals(EventPeriods.UNKNOWN, result2); - } - - @Test - void testConfigNotFound() { - EventPeriods eventPeriods = new EventPeriods(configFinder); - when(configFinder.apply("jdk.SomeEvent#period")).thenReturn(null); - Duration result = eventPeriods.getDuration("jdk.SomeEvent"); - assertEquals(EventPeriods.UNKNOWN, result); - } - - @Test - void testEveryChunk() { - // Sometimes the JFR config might have the word "everyChunk" instead of an actual value - EventPeriods eventPeriods = new EventPeriods(configFinder); - when(configFinder.apply("jdk.SomeEvent#period")).thenReturn("everyChunk"); - Duration result = eventPeriods.getDuration("jdk.SomeEvent"); - assertEquals(EventPeriods.UNKNOWN, result); - } -} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/exporter/InMemoryOtelLogger.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/exporter/InMemoryOtelLogger.java index c17f8d3a7..2ad68aeb0 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/exporter/InMemoryOtelLogger.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/exporter/InMemoryOtelLogger.java @@ -43,7 +43,7 @@ * access to the collected logs. Intended for testing use only. */ @SuppressWarnings("deprecation") // uses deprecated io.opentelemetry.sdk.logs.data.Body -class InMemoryOtelLogger implements Logger, AfterEachCallback { +public class InMemoryOtelLogger implements Logger, AfterEachCallback { private final List records = new ArrayList<>(); @Override @@ -56,7 +56,7 @@ public void afterEach(ExtensionContext extensionContext) { records.clear(); } - List records() { + public List records() { return Collections.unmodifiableList(records); } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/exporter/PprofCpuEventExporterTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/exporter/PprofCpuEventExporterTest.java new file mode 100644 index 000000000..e7caae19f --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/exporter/PprofCpuEventExporterTest.java @@ -0,0 +1,406 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.exporter; + +import static com.splunk.opentelemetry.profiler.ProfilingSemanticAttributes.FRAME_COUNT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.perftools.profiles.ProfileProto.Location; +import com.google.perftools.profiles.ProfileProto.Profile; +import com.google.perftools.profiles.ProfileProto.Sample; +import com.splunk.opentelemetry.profiler.InstrumentationSource; +import com.splunk.opentelemetry.profiler.ProfilingSemanticAttributes; +import com.splunk.opentelemetry.profiler.pprof.PprofUtils; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.trace.IdGenerator; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class PprofCpuEventExporterTest { + private final InMemoryOtelLogger logger = new InMemoryOtelLogger(); + private final PprofCpuEventExporter exporter = + new PprofCpuEventExporter.Builder() + .otelLogger(logger) + .period(Duration.ofMillis(20)) + .stackDepth(1024) + .instrumentationSource(InstrumentationSource.SNAPSHOT) + .build(); + + @Test + void noLogRecordWhenNothingToExport() { + exporter.flush(); + assertThat(logger.records()).isEmpty(); + } + + @Test + void allStackFramesAreInPprofStringTable() throws Exception { + var exception = new RuntimeException(); + + exporter.export( + 1, "thread-name", Thread.State.RUNNABLE, exception.getStackTrace(), Instant.now(), "", ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + var table = profile.getStringTableList(); + + assertThat(table).containsAll(fullyQualifiedMethodNames(exception.getStackTrace())); + } + + private List fullyQualifiedMethodNames(StackTraceElement[] stackTrace) { + return Arrays.stream(stackTrace) + .map(stackFrame -> stackFrame.getClassName() + "." + stackFrame.getMethodName()) + .collect(Collectors.toList()); + } + + @Test + void reportStackTraceWasTruncated() throws Exception { + var exception = new RuntimeException(); + var exporter = + new PprofCpuEventExporter.Builder() + .otelLogger(logger) + .period(Duration.ofMillis(20)) + .stackDepth(exception.getStackTrace().length - 1) + .instrumentationSource(InstrumentationSource.SNAPSHOT) + .build(); + + exporter.export( + 1, "thread-name", Thread.State.RUNNABLE, exception.getStackTrace(), Instant.now(), "", ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + var sample = profile.getSample(0); + + var labels = PprofUtils.toLabelString(sample, profile); + assertThat(labels) + .containsEntry(ProfilingSemanticAttributes.THREAD_STACK_TRUNCATED.getKey(), "true"); + } + + @Test + void includeOnlyTruncatedStackFrames() throws Exception { + var exception = new RuntimeException(); + var depth = exception.getStackTrace().length - 1; + var exporter = + new PprofCpuEventExporter.Builder() + .otelLogger(logger) + .period(Duration.ofMillis(20)) + .stackDepth(depth) + .instrumentationSource(InstrumentationSource.SNAPSHOT) + .build(); + + exporter.export( + 1, "thread-name", Thread.State.RUNNABLE, exception.getStackTrace(), Instant.now(), "", ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + + var expectedStackTrace = removeModuleInfo(exception.getStackTrace()).subList(0, depth); + var reportedStackTrace = toStackTrace(profile.getSample(0), profile); + for (int i = 0; i < depth; i++) { + var expectedStackFrame = expectedStackTrace.get(i); + var actualStackFrame = reportedStackTrace.get(i); + assertAll( + () -> + assertEquals( + expectedStackFrame.getClassLoaderName(), actualStackFrame.getClassLoaderName()), + () -> assertEquals(expectedStackFrame.getModuleName(), actualStackFrame.getModuleName()), + () -> + assertEquals( + expectedStackFrame.getModuleVersion(), actualStackFrame.getModuleVersion()), + () -> assertEquals(expectedStackFrame.getClassName(), actualStackFrame.getClassName()), + () -> assertEquals(expectedStackFrame.getLineNumber(), actualStackFrame.getLineNumber()), + () -> assertEquals(expectedStackFrame.getMethodName(), actualStackFrame.getMethodName()), + () -> assertEquals(expectedStackFrame.getFileName(), actualStackFrame.getFileName())); + } + } + + @Test + void allStackFramesIncludedInSample() throws Exception { + var exception = new RuntimeException(); + + exporter.export( + 1, "thread-name", Thread.State.RUNNABLE, exception.getStackTrace(), Instant.now(), "", ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + + var expectedStackTrace = removeModuleInfo(exception.getStackTrace()); + var reportedStackTrace = toStackTrace(profile.getSample(0), profile); + assertEquals(expectedStackTrace.size(), reportedStackTrace.size()); + for (int i = 0; i < expectedStackTrace.size(); i++) { + var expectedStackFrame = expectedStackTrace.get(i); + var actualStackFrame = reportedStackTrace.get(i); + assertAll( + () -> + assertEquals( + expectedStackFrame.getClassLoaderName(), actualStackFrame.getClassLoaderName()), + () -> assertEquals(expectedStackFrame.getModuleName(), actualStackFrame.getModuleName()), + () -> + assertEquals( + expectedStackFrame.getModuleVersion(), actualStackFrame.getModuleVersion()), + () -> assertEquals(expectedStackFrame.getClassName(), actualStackFrame.getClassName()), + () -> assertEquals(expectedStackFrame.getLineNumber(), actualStackFrame.getLineNumber()), + () -> assertEquals(expectedStackFrame.getMethodName(), actualStackFrame.getMethodName()), + () -> assertEquals(expectedStackFrame.getFileName(), actualStackFrame.getFileName())); + } + } + + private List toStackTrace(Sample sample, Profile profile) { + List stackTrace = new ArrayList<>(); + for (var locationId : sample.getLocationIdList()) { + var location = profile.getLocation(locationId.intValue() - 1); + stackTrace.add(toStackTrace(location, profile)); + } + return stackTrace; + } + + private StackTraceElement toStackTrace(Location location, Profile profile) { + var line = location.getLine(0); + var functionId = line.getFunctionId(); + var function = profile.getFunction((int) functionId - 1); + var fileName = profile.getStringTable((int) function.getFilename()); + var functionName = profile.getStringTable((int) function.getName()); + + var declaringClass = functionName.substring(0, functionName.lastIndexOf('.')); + var methodName = functionName.substring(functionName.lastIndexOf('.') + 1); + return new StackTraceElement(declaringClass, methodName, fileName, (int) line.getLine()); + } + + private List removeModuleInfo(StackTraceElement[] originalStackTrace) { + List stackTrace = new ArrayList<>(); + for (var stackFrame : originalStackTrace) { + stackTrace.add(removeModuleInfo(stackFrame)); + } + return stackTrace; + } + + private StackTraceElement removeModuleInfo(StackTraceElement stackFrame) { + return new StackTraceElement( + stackFrame.getClassName(), + stackFrame.getMethodName(), + valueOrUnknown(stackFrame.getFileName()), + Math.max(stackFrame.getLineNumber(), 0)); + } + + private String valueOrUnknown(String value) { + return value == null ? "unknown" : value; + } + + @Test + void maintainStackFrameCount() { + var exception = new RuntimeException(); + + exporter.export( + 1, "thread-name", Thread.State.RUNNABLE, exception.getStackTrace(), Instant.now(), "", ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + assertEquals(exception.getStackTrace().length, logRecord.getAttributes().get(FRAME_COUNT)); + } + + @Test + void maintainStackFrameCountAcrossMultipleStackTraces() { + var exception1 = new RuntimeException(); + var exception2 = new IllegalArgumentException(); + var exception3 = new IOException(); + + exporter.export( + 1, "thread-name", Thread.State.RUNNABLE, exception1.getStackTrace(), Instant.now(), "", ""); + exporter.export( + 1, "thread-name", Thread.State.RUNNABLE, exception2.getStackTrace(), Instant.now(), "", ""); + exporter.export( + 1, "thread-name", Thread.State.RUNNABLE, exception3.getStackTrace(), Instant.now(), "", ""); + exporter.flush(); + + var expectedFrameCount = + exception1.getStackTrace().length + + exception2.getStackTrace().length + + exception3.getStackTrace().length; + var logRecord = logger.records().get(0); + assertEquals(expectedFrameCount, logRecord.getAttributes().get(FRAME_COUNT)); + } + + @ParameterizedTest + @EnumSource(Thread.State.class) + void includeThreadInformationInSamples(Thread.State state) throws Exception { + var random = new Random(); + var threadId = new Random().nextLong(10_000); + var threadName = "thread-name-" + random.nextInt(1000); + + exporter.export( + threadId, threadName, state, new RuntimeException().getStackTrace(), Instant.now(), "", ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + var sample = profile.getSample(0); + + var labels = PprofUtils.toLabelString(sample, profile); + assertThat(labels).contains(entry(ProfilingSemanticAttributes.THREAD_ID, threadId)); + assertThat(labels).contains(entry(ProfilingSemanticAttributes.THREAD_NAME, threadName)); + assertThat(labels).contains(entry(ProfilingSemanticAttributes.THREAD_STATE, state.toString())); + } + + @Test + void includeTraceIdInformationInSamples() throws Exception { + var traceId = IdGenerator.random().generateTraceId(); + + exporter.export( + 1, + "thread-name", + Thread.State.RUNNABLE, + new RuntimeException().getStackTrace(), + Instant.now(), + traceId, + ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + var sample = profile.getSample(0); + + var labels = PprofUtils.toLabelString(sample, profile); + assertThat(labels).contains(entry(ProfilingSemanticAttributes.TRACE_ID, traceId)); + } + + @Test + void doNotIncludeInvalidTraceIdsInformationInSamples() throws Exception { + exporter.export( + 1, + "thread-name", + Thread.State.RUNNABLE, + new RuntimeException().getStackTrace(), + Instant.now(), + "", + ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + var sample = profile.getSample(0); + + var labels = PprofUtils.toLabelString(sample, profile); + assertThat(labels).doesNotContainKey(ProfilingSemanticAttributes.TRACE_ID.getKey()); + } + + @Test + void includeSpanIdInformationInSamples() throws Exception { + var spanId = IdGenerator.random().generateSpanId(); + + exporter.export( + 1, + "thread-name", + Thread.State.RUNNABLE, + new RuntimeException().getStackTrace(), + Instant.now(), + "", + spanId); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + var sample = profile.getSample(0); + + var labels = PprofUtils.toLabelString(sample, profile); + assertThat(labels).contains(entry(ProfilingSemanticAttributes.SPAN_ID, spanId)); + } + + @Test + void doNotIncludeInvalidSpanIdsInformationInSamples() throws Exception { + exporter.export( + 1, + "thread-name", + Thread.State.RUNNABLE, + new RuntimeException().getStackTrace(), + Instant.now(), + "", + ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + var sample = profile.getSample(0); + + var labels = PprofUtils.toLabelString(sample, profile); + assertThat(labels).doesNotContainKey(ProfilingSemanticAttributes.SPAN_ID.getKey()); + } + + @Test + void includeStackTraceTimestampInSamples() throws Exception { + var time = Instant.now(); + + exporter.export( + 1, + "thread-name", + Thread.State.RUNNABLE, + new RuntimeException().getStackTrace(), + time, + "", + ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + var sample = profile.getSample(0); + + var labels = PprofUtils.toLabelString(sample, profile); + assertThat(labels) + .contains(entry(ProfilingSemanticAttributes.SOURCE_EVENT_TIME, time.toEpochMilli())); + } + + @Test + void includeStackTraceDurationInSamples() throws Exception { + exporter.export( + 1, + "thread-name", + Thread.State.RUNNABLE, + new RuntimeException().getStackTrace(), + Instant.now(), + "", + ""); + exporter.flush(); + + var logRecord = logger.records().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + var sample = profile.getSample(0); + + var labels = PprofUtils.toLabelString(sample, profile); + assertThat(labels) + .contains( + entry( + ProfilingSemanticAttributes.SOURCE_EVENT_PERIOD, Duration.ofMillis(20).toMillis())); + } + + private Map.Entry entry(AttributeKey attribute, T value) { + return Map.entry(attribute.getKey(), value); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/pprof/PprofUtils.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/pprof/PprofUtils.java new file mode 100644 index 000000000..d0758272e --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/pprof/PprofUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.pprof; + +import com.google.perftools.profiles.ProfileProto.Profile; +import com.google.perftools.profiles.ProfileProto.Sample; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +public class PprofUtils { + public static byte[] deserialize(LogRecordData logRecord) throws IOException { + var bytes = new ByteArrayInputStream(decode(logRecord)); + var inputStream = new GZIPInputStream(bytes); + return inputStream.readAllBytes(); + } + + public static byte[] decode(LogRecordData logRecord) { + Value body = logRecord.getBodyValue(); + if (body == null) { + throw new RuntimeException("Log record body is null"); + } + return Base64.getDecoder().decode(body.asString()); + } + + public static Map toLabelString(Sample sample, Profile profile) { + var labels = new HashMap(); + for (var label : sample.getLabelList()) { + var stringTableIndex = label.getKey(); + var key = profile.getStringTable((int) stringTableIndex); + if (label.getStr() > 0) { + labels.put(key, profile.getStringTable((int) label.getStr())); + } else { + labels.put(key, label.getNum()); + } + } + return labels; + } + + private PprofUtils() {} +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AccumulatingStagingAreaTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AccumulatingStagingAreaTest.java new file mode 100644 index 000000000..7177abda6 --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AccumulatingStagingAreaTest.java @@ -0,0 +1,102 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.sdk.trace.IdGenerator; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class AccumulatingStagingAreaTest { + private final IdGenerator idGenerator = IdGenerator.random(); + private final InMemoryStackTraceExporter exporter = new InMemoryStackTraceExporter(); + private final AccumulatingStagingArea stagingArea = new AccumulatingStagingArea(() -> exporter); + + @Test + void exportStackTracesToLogExporter() { + var traceId = idGenerator.generateTraceId(); + var stackTrace = Snapshotting.stackTrace().build(); + + stagingArea.stage(traceId, stackTrace); + stagingArea.empty(traceId); + + assertEquals(List.of(stackTrace), exporter.stackTraces()); + } + + @Test + void onlyExportStackTracesWhenAtLeastOneHasBeenStaged() { + var traceId = idGenerator.generateTraceId(); + stagingArea.empty(traceId); + assertEquals(Collections.emptyList(), exporter.stackTraces()); + } + + @Test + void exportMultipleStackTracesToLogExporter() { + var traceId = idGenerator.generateTraceId(); + var stackTrace1 = Snapshotting.stackTrace().withId(1).withName("one").build(); + var stackTrace2 = Snapshotting.stackTrace().withId(1).withName("two").build(); + + stagingArea.stage(traceId, stackTrace1); + stagingArea.stage(traceId, stackTrace2); + stagingArea.empty(traceId); + + assertEquals(List.of(stackTrace1, stackTrace2), exporter.stackTraces()); + } + + @Test + void exportStackTracesForOnlySpecifiedThread() { + var traceId1 = idGenerator.generateTraceId(); + var traceId2 = idGenerator.generateTraceId(); + var stackTrace1 = Snapshotting.stackTrace().withId(1).withName("one").build(); + var stackTrace2 = Snapshotting.stackTrace().withId(1).withName("two").build(); + + stagingArea.stage(traceId1, stackTrace1); + stagingArea.stage(traceId2, stackTrace2); + stagingArea.empty(traceId1); + + assertEquals(List.of(stackTrace1), exporter.stackTraces()); + } + + @Test + void exportStackTracesForMultipleThreads() { + var traceId1 = idGenerator.generateTraceId(); + var traceId2 = idGenerator.generateTraceId(); + var stackTrace1 = Snapshotting.stackTrace().withId(1).withName("one").build(); + var stackTrace2 = Snapshotting.stackTrace().withId(1).withName("two").build(); + + stagingArea.stage(traceId1, stackTrace1); + stagingArea.stage(traceId2, stackTrace2); + stagingArea.empty(traceId1); + stagingArea.empty(traceId2); + + assertEquals(List.of(stackTrace1, stackTrace2), exporter.stackTraces()); + } + + @Test + void stackTracesAreNotExportedMultipleTimes() { + var traceId = idGenerator.generateTraceId(); + var stackTrace = Snapshotting.stackTrace().build(); + + stagingArea.stage(traceId, stackTrace); + stagingArea.empty(traceId); + stagingArea.empty(traceId); + + assertEquals(List.of(stackTrace), exporter.stackTraces()); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AsyncStackTraceExporterTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AsyncStackTraceExporterTest.java new file mode 100644 index 000000000..14ec0b56b --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AsyncStackTraceExporterTest.java @@ -0,0 +1,174 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import static com.splunk.opentelemetry.profiler.ProfilingSemanticAttributes.DATA_FORMAT; +import static com.splunk.opentelemetry.profiler.ProfilingSemanticAttributes.DATA_TYPE; +import static com.splunk.opentelemetry.profiler.ProfilingSemanticAttributes.FRAME_COUNT; +import static com.splunk.opentelemetry.profiler.ProfilingSemanticAttributes.INSTRUMENTATION_SOURCE; +import static com.splunk.opentelemetry.profiler.ProfilingSemanticAttributes.SOURCE_EVENT_TIME; +import static com.splunk.opentelemetry.profiler.ProfilingSemanticAttributes.SOURCE_TYPE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.perftools.profiles.ProfileProto.Profile; +import com.splunk.opentelemetry.profiler.exporter.InMemoryOtelLogger; +import com.splunk.opentelemetry.profiler.pprof.PprofUtils; +import java.io.ByteArrayInputStream; +import java.time.Duration; +import java.util.List; +import java.util.zip.GZIPInputStream; +import org.junit.jupiter.api.Test; + +class AsyncStackTraceExporterTest { + private final InMemoryOtelLogger logger = new InMemoryOtelLogger(); + private final AsyncStackTraceExporter exporter = + new AsyncStackTraceExporter(logger, Duration.ofMillis(20), 200); + + @Test + void exportStackTraceAsOpenTelemetryLog() { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + assertEquals(1, logger.records().size()); + } + + @Test + void exportMultipleStackTraceAsSingleOpenTelemetryLog() { + var one = Snapshotting.stackTrace().with(new RuntimeException()).build(); + var two = Snapshotting.stackTrace().with(new IllegalArgumentException()).build(); + var three = Snapshotting.stackTrace().with(new NullPointerException()).build(); + + exporter.export(List.of(one, two, three)); + await().until(() -> !logger.records().isEmpty()); + + assertEquals(1, logger.records().size()); + } + + @Test + void encodedLogBodyIsPprofProtobufMessage() { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + var logRecord = logger.records().get(0); + assertDoesNotThrow(() -> Profile.parseFrom(PprofUtils.deserialize(logRecord))); + } + + @Test + void encodeLogBodyUsingBase64() { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + var logRecord = logger.records().get(0); + assertDoesNotThrow(() -> PprofUtils.decode(logRecord)); + } + + @Test + void logBodyIsGZipped() { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + var logRecord = logger.records().get(0); + assertDoesNotThrow( + () -> { + var bytes = new ByteArrayInputStream(PprofUtils.decode(logRecord)); + var inputStream = new GZIPInputStream(bytes); + inputStream.readAllBytes(); + }); + } + + @Test + void includeSourceTypeOpenTelemetryAttribute() { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + var attributes = logger.records().get(0).getAttributes(); + assertThat(attributes.asMap()).containsEntry(SOURCE_TYPE, "otel.profiling"); + } + + @Test + void includeDataTypeOpenTelemetryAttributeWithValueOfCpu() { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + var attributes = logger.records().get(0).getAttributes(); + assertThat(attributes.asMap()).containsEntry(DATA_TYPE, "cpu"); + } + + @Test + void includeDataFormatOpenTelemetryAttributeWithValueOfGzipBase64() { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + var attributes = logger.records().get(0).getAttributes(); + assertThat(attributes.asMap()).containsEntry(DATA_FORMAT, "pprof-gzip-base64"); + } + + @Test + void includeInstrumentationSourceOpenTelemetryAttributeWithValueOfSnapshot() { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + var attributes = logger.records().get(0).getAttributes(); + assertThat(attributes.asMap()).containsEntry(INSTRUMENTATION_SOURCE, "snapshot"); + } + + @Test + void includeFrameCountOpenTelemetryAttributeInLogMessage() { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + var attributes = logger.records().get(0).getAttributes(); + assertThat(attributes.asMap()) + .containsEntry(FRAME_COUNT, (long) stackTrace.getStackFrames().length); + } + + @Test + void includeStackTraceDurationInSamples() throws Exception { + var stackTrace = Snapshotting.stackTrace().build(); + + exporter.export(List.of(stackTrace)); + await().until(() -> !logger.records().isEmpty()); + + var profile = Profile.parseFrom(PprofUtils.deserialize(logger.records().get(0))); + var sample = profile.getSample(0); + + var labels = PprofUtils.toLabelString(sample, profile); + assertThat(labels) + .containsEntry(SOURCE_EVENT_TIME.getKey(), stackTrace.getTimestamp().toEpochMilli()); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AutoConfigureSnapshotVolumePropagatorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AutoConfigureSnapshotVolumePropagatorTest.java index 86a82aa5d..f1cb29ff9 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AutoConfigureSnapshotVolumePropagatorTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/AutoConfigureSnapshotVolumePropagatorTest.java @@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.assertThat; import com.splunk.opentelemetry.profiler.Configuration; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension.Builder; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -29,7 +31,7 @@ class AutoConfigureSnapshotVolumePropagatorTest { @Test void autoConfigureSnapshotVolumePropagator() { try (var sdk = newSdk().build()) { - var properties = sdk.getProperties(); + var properties = sdk.getConfig(); assertThat(properties.getList(OTEL_PROPAGATORS)) .contains(SnapshotVolumePropagatorProvider.NAME); } @@ -38,7 +40,7 @@ void autoConfigureSnapshotVolumePropagator() { @Test void snapshotVolumePropagatorMustBeAfterTraceContextAndBaggage() { try (var sdk = newSdk().build()) { - var properties = sdk.getProperties(); + var properties = sdk.getConfig(); assertThat(properties.getList(OTEL_PROPAGATORS)) .containsExactly("tracecontext", "baggage", SnapshotVolumePropagatorProvider.NAME); } @@ -50,7 +52,7 @@ void appendSnapshotPropagatorToEndOfAlreadyConfiguredPropagators() { newSdk() .withProperty(OTEL_PROPAGATORS, "tracecontext,baggage,some-other-propagator") .build()) { - var properties = sdk.getProperties(); + var properties = sdk.getConfig(); assertThat(properties.getList(OTEL_PROPAGATORS)) .containsExactly( "tracecontext", @@ -64,7 +66,7 @@ void appendSnapshotPropagatorToEndOfAlreadyConfiguredPropagators() { @ValueSource(strings = {"baggage", "tracecontext"}) void doNotDoubleCountDefaultOpenTelemetryPropagators(String propagatorName) { try (var sdk = newSdk().withProperty(OTEL_PROPAGATORS, propagatorName).build()) { - var properties = sdk.getProperties(); + var properties = sdk.getConfig(); assertThat(properties.getList(OTEL_PROPAGATORS)).containsOnlyOnce(propagatorName); } } @@ -73,7 +75,7 @@ void doNotDoubleCountDefaultOpenTelemetryPropagators(String propagatorName) { void doNotDoubleCountSnapshotVolumePropagator() { try (var sdk = newSdk().withProperty(OTEL_PROPAGATORS, SnapshotVolumePropagatorProvider.NAME).build()) { - var properties = sdk.getProperties(); + var properties = sdk.getConfig(); assertThat(properties.getList(OTEL_PROPAGATORS)) .containsOnlyOnce(SnapshotVolumePropagatorProvider.NAME); } @@ -83,7 +85,7 @@ void doNotDoubleCountSnapshotVolumePropagator() { void doNotAddSnapshotVolumePropagatorWhenTraceSnapshottingIsDisabled() { try (var sdk = newSdk().withProperty(Configuration.CONFIG_KEY_ENABLE_SNAPSHOT_PROFILER, "false").build()) { - var properties = sdk.getProperties(); + var properties = sdk.getConfig(); assertThat(properties.getList(OTEL_PROPAGATORS)) .doesNotContain(SnapshotVolumePropagatorProvider.NAME); } @@ -92,7 +94,7 @@ void doNotAddSnapshotVolumePropagatorWhenTraceSnapshottingIsDisabled() { @Test void doNotAddSnapshotVolumePropagatorsConfiguredAsNone() { try (var sdk = newSdk().withProperty(OTEL_PROPAGATORS, "none").build()) { - var properties = sdk.getProperties(); + var properties = sdk.getConfig(); assertThat(properties.getList(OTEL_PROPAGATORS)) .doesNotContain(SnapshotVolumePropagatorProvider.NAME); } @@ -102,7 +104,7 @@ void doNotAddSnapshotVolumePropagatorsConfiguredAsNone() { void doNotAddTraceContextPropagatorWhenOtherPropagatorsAreExplicitlyConfigured() { try (var sdk = newSdk().withProperty(OTEL_PROPAGATORS, "some-other-propagator,baggage").build()) { - var properties = sdk.getProperties(); + var properties = sdk.getConfig(); assertThat(properties.getList(OTEL_PROPAGATORS)) .containsExactly( "some-other-propagator", "baggage", SnapshotVolumePropagatorProvider.NAME); @@ -112,15 +114,15 @@ void doNotAddTraceContextPropagatorWhenOtherPropagatorsAreExplicitlyConfigured() @Test void addBaggagePropagatorWhenOtherPropagatorsAreExplicitlyConfiguredButBaggageIsMissing() { try (var sdk = newSdk().withProperty(OTEL_PROPAGATORS, "some-other-propagator").build()) { - var properties = sdk.getProperties(); + var properties = sdk.getConfig(); assertThat(properties.getList(OTEL_PROPAGATORS)) .containsExactly( "some-other-propagator", "baggage", SnapshotVolumePropagatorProvider.NAME); } } - private OpenTelemetrySdkExtension.Builder newSdk() { - return OpenTelemetrySdkExtension.builder() + private Builder newSdk() { + return OpenTelemetrySdkExtension.configure() .withProperty(Configuration.CONFIG_KEY_ENABLE_SNAPSHOT_PROFILER, "true") .with(new SnapshotProfilingSdkCustomizer()); } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/DistributedProfilingSignalTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/DistributedProfilingSignalTest.java index 41be44c01..a21b951be 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/DistributedProfilingSignalTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/DistributedProfilingSignalTest.java @@ -23,6 +23,7 @@ import com.splunk.opentelemetry.profiler.snapshot.simulation.ExitCall; import com.splunk.opentelemetry.profiler.snapshot.simulation.Message; import com.splunk.opentelemetry.profiler.snapshot.simulation.Server; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; import java.time.Duration; import java.util.function.UnaryOperator; import org.junit.jupiter.api.Test; @@ -35,7 +36,7 @@ class DistributedProfilingSignalTest { @RegisterExtension public final OpenTelemetrySdkExtension downstreamSdk = - OpenTelemetrySdkExtension.builder() + OpenTelemetrySdkExtension.configure() .withProperty("splunk.snapshot.profiler.enabled", "true") .with(downstreamCustomizer) .with(new SnapshotVolumePropagator(() -> true)) @@ -49,7 +50,7 @@ class DistributedProfilingSignalTest { .build(); @RegisterExtension - public final OpenTelemetrySdkExtension middleSdk = OpenTelemetrySdkExtension.builder().build(); + public final OpenTelemetrySdkExtension middleSdk = OpenTelemetrySdkExtension.configure().build(); @RegisterExtension public final Server middle = @@ -61,7 +62,7 @@ class DistributedProfilingSignalTest { @RegisterExtension public final OpenTelemetrySdkExtension upstreamSdk = - OpenTelemetrySdkExtension.builder() + OpenTelemetrySdkExtension.configure() .withProperty("splunk.snapshot.profiler.enabled", "true") .with(upstreamCustomizer) .with(new SnapshotVolumePropagator(() -> true)) diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/InMemoryStackTraceExporter.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/InMemoryStackTraceExporter.java new file mode 100644 index 000000000..f05434697 --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/InMemoryStackTraceExporter.java @@ -0,0 +1,38 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * In memory implementation of the {@link StackTraceExporter} interface that allows for direct + * access to the exported {@link StackTrace}s. Intended for testing use only. + */ +class InMemoryStackTraceExporter implements StackTraceExporter { + private final List stackTraces = new ArrayList<>(); + + @Override + public void export(List stackTraces) { + this.stackTraces.addAll(stackTraces); + } + + List stackTraces() { + return Collections.unmodifiableList(stackTraces); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/ScheduledExecutorStackTraceSamplerTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/ScheduledExecutorStackTraceSamplerTest.java index c5c7e3098..6ec211b6a 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/ScheduledExecutorStackTraceSamplerTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/ScheduledExecutorStackTraceSamplerTest.java @@ -16,28 +16,32 @@ package com.splunk.opentelemetry.profiler.snapshot; +import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.sdk.trace.IdGenerator; import java.time.Duration; +import java.time.Instant; import java.util.Collections; +import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; class ScheduledExecutorStackTraceSamplerTest { - private static final Duration HALF_SECOND = Duration.ofMillis(500); - private static final Duration PERIOD = Duration.ofMillis(20); + private static final Duration SAMPLING_PERIOD = Duration.ofMillis(20); private final IdGenerator idGenerator = IdGenerator.random(); private final InMemoryStagingArea staging = new InMemoryStagingArea(); private final ScheduledExecutorStackTraceSampler sampler = - new ScheduledExecutorStackTraceSampler(staging, PERIOD); + new ScheduledExecutorStackTraceSampler(staging, SAMPLING_PERIOD); @Test void takeStackTraceSampleForGivenThread() { @@ -45,7 +49,7 @@ void takeStackTraceSampleForGivenThread() { try { sampler.start(spanContext); - await().atMost(HALF_SECOND).until(() -> !staging.allStackTraces().isEmpty()); + await().until(() -> !staging.allStackTraces().isEmpty()); } finally { sampler.stop(spanContext); } @@ -53,12 +57,13 @@ void takeStackTraceSampleForGivenThread() { @Test void continuallySampleThreadForStackTraces() { + var halfSecond = Duration.ofMillis(500); var spanContext = randomSpanContext(); - int expectedSamples = (int) HALF_SECOND.dividedBy(PERIOD.multipliedBy(2)); + int expectedSamples = (int) halfSecond.dividedBy(SAMPLING_PERIOD.multipliedBy(2)); try { sampler.start(spanContext); - await().atMost(HALF_SECOND).until(() -> staging.allStackTraces().size() >= expectedSamples); + await().until(() -> staging.allStackTraces().size() >= expectedSamples); } finally { sampler.stop(spanContext); } @@ -66,12 +71,13 @@ void continuallySampleThreadForStackTraces() { @Test void emptyStagingAreaAfterSamplingStops() { + var halfSecond = Duration.ofMillis(500); var spanContext = randomSpanContext(); - int expectedSamples = (int) HALF_SECOND.dividedBy(PERIOD.multipliedBy(2)); + int expectedSamples = (int) halfSecond.dividedBy(SAMPLING_PERIOD.multipliedBy(2)); try { sampler.start(spanContext); - await().atMost(HALF_SECOND).until(() -> staging.allStackTraces().size() >= expectedSamples); + await().until(() -> staging.allStackTraces().size() >= expectedSamples); } finally { sampler.stop(spanContext); } @@ -108,19 +114,118 @@ void onlyTakeStackTraceSamplesForOneThreadPerTrace() { } } - private Runnable startSampling( + @Test + void includeTimestampOnStackTraces() { + var now = Instant.now(); + var spanContext = randomSpanContext(); + + try { + sampler.start(spanContext); + await().until(() -> !staging.allStackTraces().isEmpty()); + + var stackTrace = staging.allStackTraces().stream().findFirst().orElseThrow(); + assertThat(stackTrace.getTimestamp()).isNotNull().isAfter(now); + } finally { + sampler.stop(spanContext); + } + } + + @Test + void includeDefaultSamplingPeriodOnFirstRecordedStackTraces() { + var spanContext = randomSpanContext(); + + try { + sampler.start(spanContext); + await().until(() -> !staging.allStackTraces().isEmpty()); + + var stackTrace = staging.allStackTraces().stream().findFirst().orElseThrow(); + assertThat(stackTrace.getDuration()).isNotNull().isEqualTo(SAMPLING_PERIOD); + } finally { + sampler.stop(spanContext); + } + } + + @Test + void calculateSamplingPeriodAfterFirstRecordedStackTraces() { + var spanContext = randomSpanContext(); + + try { + sampler.start(spanContext); + await().until(() -> staging.allStackTraces().size() > 1); + + var stackTrace = staging.allStackTraces().stream().skip(1).findFirst().orElseThrow(); + assertThat(stackTrace.getDuration()).isNotNull().isEqualTo(SAMPLING_PERIOD); + } finally { + sampler.stop(spanContext); + } + } + + @Test + void includeTraceIdOnStackTraces() { + var spanContext = randomSpanContext(); + + try { + sampler.start(spanContext); + await().until(() -> !staging.allStackTraces().isEmpty()); + + var stackTrace = staging.allStackTraces().stream().findFirst().orElseThrow(); + assertEquals(spanContext.getTraceId(), stackTrace.getTraceId()); + } finally { + sampler.stop(spanContext); + } + } + + @Test + void includeThreadDetailsOnStackTraces() throws Exception { + var executor = Executors.newSingleThreadExecutor(); + var traceId = idGenerator.generateTraceId(); + var spanContext = randomSpanContext(traceId); + var startLatch = new CountDownLatch(1); + var stopLatch = new CountDownLatch(1); + try { + var future = executor.submit(startSampling(spanContext, startLatch, stopLatch)); + + startLatch.countDown(); + await().until(() -> !staging.allStackTraces().isEmpty()); + stopLatch.countDown(); + + var thread = future.get(); + var stackTrace = staging.allStackTraces().stream().findFirst().orElseThrow(); + assertAll( + () -> assertEquals(thread.id, stackTrace.getThreadId()), + () -> assertEquals(thread.name, stackTrace.getThreadName()), + () -> assertNotNull(stackTrace.getThreadState()), + () -> assertThat(stackTrace.getStackFrames()).isNotEmpty()); + } finally { + sampler.stop(spanContext); + executor.shutdownNow(); + } + } + + private Callable startSampling( SpanContext spanContext, CountDownLatch startSpanLatch, CountDownLatch shutdownLatch) { return (() -> { try { startSpanLatch.await(); sampler.start(spanContext); shutdownLatch.await(); + return new ThreadInfo(Thread.currentThread().getId(), Thread.currentThread().getName()); } catch (InterruptedException e) { throw new RuntimeException(e); } }); } + private static class ThreadInfo { + public final long id; + public final String name; + + private ThreadInfo(long id, String name) { + this.id = id; + this.name = name; + } + } + private SpanContext randomSpanContext() { return randomSpanContext(idGenerator.generateTraceId()); } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingFeatureFlagTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingFeatureFlagTest.java index 8ea45ceef..05386c643 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingFeatureFlagTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingFeatureFlagTest.java @@ -21,6 +21,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -34,7 +35,7 @@ class SnapshotProfilingFeatureFlagTest { class SnapshotProfilingDisabledByDefaultTest { @RegisterExtension public final OpenTelemetrySdkExtension s = - OpenTelemetrySdkExtension.builder().with(customizer).build(); + OpenTelemetrySdkExtension.configure().with(customizer).build(); @ParameterizedTest @SpanKinds.Entry @@ -50,7 +51,7 @@ void snapshotProfilingIsDisabledByDefault(SpanKind kind, Tracer tracer) { class SnapshotProfilingEnabledTest { @RegisterExtension public final OpenTelemetrySdkExtension s = - OpenTelemetrySdkExtension.builder() + OpenTelemetrySdkExtension.configure() .with(customizer) .withProperty("splunk.snapshot.profiler.enabled", "true") .build(); @@ -69,7 +70,7 @@ void snapshotProfilingIsExplicitlyEnabled(SpanKind kind, Tracer tracer) { class SnapshotProfilingDisabledTest { @RegisterExtension public final OpenTelemetrySdkExtension s = - OpenTelemetrySdkExtension.builder() + OpenTelemetrySdkExtension.configure() .with(customizer) .withProperty("splunk.snapshot.profiler.enabled", "false") .build(); diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingLogExportingTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingLogExportingTest.java new file mode 100644 index 000000000..6dadb86b8 --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingLogExportingTest.java @@ -0,0 +1,89 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import static com.google.perftools.profiles.ProfileProto.Sample; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.perftools.profiles.ProfileProto.Profile; +import com.splunk.opentelemetry.profiler.OtelLoggerFactory; +import com.splunk.opentelemetry.profiler.ProfilingSemanticAttributes; +import com.splunk.opentelemetry.profiler.pprof.PprofUtils; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; + +class SnapshotProfilingLogExportingTest { + private static final Predicate> TRACE_ID_LABEL = + kv -> ProfilingSemanticAttributes.TRACE_ID.getKey().equals(kv.getKey()); + + private final InMemoryLogRecordExporter logExporter = InMemoryLogRecordExporter.create(); + + @RegisterExtension + public final OpenTelemetrySdkExtension sdk = + OpenTelemetrySdkExtension.configure() + .withProperty("splunk.snapshot.profiler.enabled", "true") + .with(Snapshotting.customizer().withRealStackTraceSampler().build()) + .with(new StackTraceExporterActivator(new OtelLoggerFactory(properties -> logExporter))) + .build(); + + @AfterEach + void tearDown() { + StackTraceExporterProvider.INSTANCE.reset(); + } + + @ParameterizedTest + @SpanKinds.Entry + void exportStackTracesForProfiledTraces(SpanKind kind, Tracer tracer) throws Exception { + String traceId; + try (var ignored = Context.root().with(Volume.HIGHEST).makeCurrent()) { + var span = tracer.spanBuilder("root").setSpanKind(kind).startSpan(); + traceId = span.getSpanContext().getTraceId(); + Thread.sleep(250); + span.end(); + } + + await().until(() -> !logExporter.getFinishedLogRecordItems().isEmpty()); + + var logRecord = logExporter.getFinishedLogRecordItems().get(0); + var profile = Profile.parseFrom(PprofUtils.deserialize(logRecord)); + + var traceIds = + profile.getSampleList().stream() + .flatMap(sampleLabels(profile)) + .filter(TRACE_ID_LABEL) + .map(Map.Entry::getValue) + .collect(Collectors.toSet()); + assertEquals(Set.of(traceId), traceIds); + } + + private Function>> sampleLabels(Profile profile) { + return sample -> PprofUtils.toLabelString(sample, profile).entrySet().stream(); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingSdkCustomizerBuilder.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingSdkCustomizerBuilder.java index e9096490c..a639bdbdc 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingSdkCustomizerBuilder.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotProfilingSdkCustomizerBuilder.java @@ -16,6 +16,8 @@ package com.splunk.opentelemetry.profiler.snapshot; +import java.time.Duration; + class SnapshotProfilingSdkCustomizerBuilder { private TraceRegistry registry = new TraceRegistry(); private StackTraceSampler sampler = new ObservableStackTraceSampler(); @@ -25,6 +27,13 @@ SnapshotProfilingSdkCustomizerBuilder with(TraceRegistry registry) { return this; } + SnapshotProfilingSdkCustomizerBuilder withRealStackTraceSampler() { + return with( + new ScheduledExecutorStackTraceSampler( + new AccumulatingStagingArea(StackTraceExporterProvider.INSTANCE), + Duration.ofMillis(20))); + } + SnapshotProfilingSdkCustomizerBuilder with(StackTraceSampler sampler) { this.sampler = sampler; return this; diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotSpanAttributeTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotSpanAttributeTest.java index 57ec1314a..d8828c1c8 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotSpanAttributeTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotSpanAttributeTest.java @@ -23,6 +23,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; import io.opentelemetry.sdk.trace.ReadWriteSpan; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -34,7 +35,7 @@ class SnapshotSpanAttributeTest { @RegisterExtension public final OpenTelemetrySdkExtension s = - OpenTelemetrySdkExtension.builder() + OpenTelemetrySdkExtension.configure() .with(customizer) .withProperty("splunk.snapshot.profiler.enabled", "true") .build(); diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotVolumePropagatorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotVolumePropagatorTest.java index 70e980105..6fcea4d99 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotVolumePropagatorTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SnapshotVolumePropagatorTest.java @@ -22,13 +22,14 @@ import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; import java.util.Collections; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; class SnapshotVolumePropagatorTest { @RegisterExtension - public final OpenTelemetrySdkExtension sdk = OpenTelemetrySdkExtension.builder().build(); + public final OpenTelemetrySdkExtension sdk = OpenTelemetrySdkExtension.configure().build(); @RegisterExtension public final ObservableCarrier carrier = new ObservableCarrier(); diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/Snapshotting.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/Snapshotting.java index 6688199fc..9dfdc4d09 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/Snapshotting.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/Snapshotting.java @@ -16,10 +16,34 @@ package com.splunk.opentelemetry.profiler.snapshot; +import io.opentelemetry.sdk.trace.IdGenerator; +import java.time.Duration; +import java.time.Instant; +import java.util.Random; + class Snapshotting { + private static final Random RANDOM = new Random(); + private static final IdGenerator ID_GENERATOR = IdGenerator.random(); + static SnapshotProfilingSdkCustomizerBuilder customizer() { return new SnapshotProfilingSdkCustomizerBuilder(); } + static StackTraceBuilder stackTrace() { + var threadId = RANDOM.nextLong(10_000); + return new StackTraceBuilder() + .with(Instant.now()) + .with(Duration.ofMillis(20)) + .withTraceId(randomTraceId()) + .withId(threadId) + .withName("thread-" + threadId) + .with(Thread.State.WAITING) + .with(new RuntimeException()); + } + + static String randomTraceId() { + return ID_GENERATOR.generateTraceId(); + } + private Snapshotting() {} } diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SpanSamplingTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SpanSamplingTest.java index 2d514e214..42b3fa8dc 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SpanSamplingTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/SpanSamplingTest.java @@ -21,6 +21,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; import io.opentelemetry.sdk.trace.samplers.Sampler; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.extension.RegisterExtension; @@ -35,7 +36,7 @@ class SpanSamplingTest { class SpanSamplingDisabled { @RegisterExtension public final OpenTelemetrySdkExtension s = - OpenTelemetrySdkExtension.builder() + OpenTelemetrySdkExtension.configure() .withProperty("splunk.snapshot.profiler.enabled", "true") .withSampler(Sampler.alwaysOff()) .with(customizer) @@ -55,7 +56,7 @@ void doNotRegisterTraceForProfilingWhenSpanSamplingIsOff(SpanKind kind, Tracer t class SpanSamplingEnabled { @RegisterExtension public final OpenTelemetrySdkExtension s = - OpenTelemetrySdkExtension.builder() + OpenTelemetrySdkExtension.configure() .withProperty("splunk.snapshot.profiler.enabled", "true") .withSampler(Sampler.alwaysOn()) .with(customizer) diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceBuilder.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceBuilder.java new file mode 100644 index 000000000..5143b3b27 --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceBuilder.java @@ -0,0 +1,70 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import java.time.Duration; +import java.time.Instant; + +class StackTraceBuilder { + private Instant timestamp; + private Duration duration; + private String traceId; + private long threadId; + private String threadName; + private Thread.State state; + private Exception exception; + + public StackTraceBuilder with(Instant timestamp) { + this.timestamp = timestamp; + return this; + } + + public StackTraceBuilder with(Duration duration) { + this.duration = duration; + return this; + } + + public StackTraceBuilder withTraceId(String traceId) { + this.traceId = traceId; + return this; + } + + public StackTraceBuilder withId(long threadId) { + this.threadId = threadId; + return this; + } + + public StackTraceBuilder withName(String threadName) { + this.threadName = threadName; + return this; + } + + public StackTraceBuilder with(Thread.State state) { + this.state = state; + return this; + } + + public StackTraceBuilder with(Exception exception) { + this.exception = exception; + return this; + } + + StackTrace build() { + return new StackTrace( + timestamp, duration, traceId, threadId, threadName, state, exception.getStackTrace()); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterActivatorTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterActivatorTest.java new file mode 100644 index 000000000..b2bf6fdda --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterActivatorTest.java @@ -0,0 +1,69 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class StackTraceExporterActivatorTest { + @AfterEach + void tearDown() { + StackTraceExporterProvider.INSTANCE.reset(); + } + + @Nested + class SnapshotProfilingEnabled { + @RegisterExtension + public final OpenTelemetrySdkExtension s = + OpenTelemetrySdkExtension.configure() + .withProperty("splunk.snapshot.profiler.enabled", "true") + .with(new StackTraceExporterActivator()) + .build(); + + @Test + void configureStackTraceExporterProvider() { + System.out.println("one"); + var exporter = StackTraceExporterProvider.INSTANCE.get(); + assertNotSame(StackTraceExporter.NOOP, exporter); + assertInstanceOf(AsyncStackTraceExporter.class, exporter); + } + } + + @Nested + class SnapshotProfilingDisabled { + @RegisterExtension + public final OpenTelemetrySdkExtension s = + OpenTelemetrySdkExtension.configure() + .withProperty("splunk.snapshot.profiler.enabled", "false") + .with(new StackTraceExporterActivator()) + .build(); + + @Test + void doNotConfigureStackTraceExporterProvider() { + System.out.println("two"); + var exporter = StackTraceExporterProvider.INSTANCE.get(); + assertSame(StackTraceExporter.NOOP, exporter); + } + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterProviderTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterProviderTest.java new file mode 100644 index 000000000..a6f5ebd24 --- /dev/null +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/StackTraceExporterProviderTest.java @@ -0,0 +1,50 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.profiler.snapshot; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class StackTraceExporterProviderTest { + private final StackTraceExporterProvider provider = StackTraceExporterProvider.INSTANCE; + + @AfterEach + void tearDown() { + provider.reset(); + } + + @Test + void provideNoopExporterWhenNotConfigured() { + assertSame(StackTraceExporter.NOOP, provider.get()); + } + + @Test + void providedConfiguredExporter() { + var exporter = new InMemoryStackTraceExporter(); + provider.configure(exporter); + assertSame(exporter, provider.get()); + } + + @Test + void canResetConfiguredExporter() { + provider.configure(new InMemoryStackTraceExporter()); + provider.reset(); + assertSame(StackTraceExporter.NOOP, provider.get()); + } +} diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/TraceProfilingTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/TraceProfilingTest.java index e8d824c8a..66d13a12a 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/TraceProfilingTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/TraceProfilingTest.java @@ -22,6 +22,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -33,7 +34,7 @@ class TraceProfilingTest { @RegisterExtension public final OpenTelemetrySdkExtension sdk = - OpenTelemetrySdkExtension.builder() + OpenTelemetrySdkExtension.configure() .withProperty("splunk.snapshot.profiler.enabled", "true") .with(customizer) .build(); diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/TraceRegistrationTest.java b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/TraceRegistrationTest.java index c3c677f8c..4bec19cdf 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/TraceRegistrationTest.java +++ b/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/TraceRegistrationTest.java @@ -21,6 +21,7 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkExtension; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -31,7 +32,7 @@ class TraceRegistrationTest { @RegisterExtension public final OpenTelemetrySdkExtension s = - OpenTelemetrySdkExtension.builder() + OpenTelemetrySdkExtension.configure() .withProperty("splunk.snapshot.profiler.enabled", "true") .with(customizer) .build(); diff --git a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/OpenTelemetrySdkExtension.java b/profiler/src/test/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkExtension.java similarity index 86% rename from profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/OpenTelemetrySdkExtension.java rename to profiler/src/test/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkExtension.java index 519124ff3..7ed4194c0 100644 --- a/profiler/src/test/java/com/splunk/opentelemetry/profiler/snapshot/OpenTelemetrySdkExtension.java +++ b/profiler/src/test/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySdkExtension.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.opentelemetry.profiler.snapshot; +package io.opentelemetry.sdk.autoconfigure; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; @@ -25,6 +25,7 @@ import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.javaagent.extension.AgentListener; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; @@ -44,22 +45,30 @@ import java.util.function.Function; import java.util.function.Supplier; import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; -public class OpenTelemetrySdkExtension - implements OpenTelemetry, AfterEachCallback, ParameterResolver, AutoCloseable { - public static Builder builder() { +public class OpenTelemetrySdkExtension extends AutoConfiguredOpenTelemetrySdk + implements OpenTelemetry, + BeforeEachCallback, + AfterEachCallback, + ParameterResolver, + AutoCloseable { + public static Builder configure() { return new Builder(); } private final OpenTelemetrySdk sdk; private final ConfigProperties properties; + private final List agentListeners; - private OpenTelemetrySdkExtension(OpenTelemetrySdk sdk, ConfigProperties properties) { + private OpenTelemetrySdkExtension( + OpenTelemetrySdk sdk, ConfigProperties properties, List agentListeners) { this.sdk = sdk; this.properties = properties; + this.agentListeners = agentListeners; } @Override @@ -82,6 +91,11 @@ public ContextPropagators getPropagators() { return sdk.getPropagators(); } + @Override + public void beforeEach(ExtensionContext context) { + agentListeners.forEach(listener -> listener.afterAgent(this)); + } + @Override public void afterEach(ExtensionContext extensionContext) { sdk.close(); @@ -92,10 +106,26 @@ public void close() { sdk.close(); } - public ConfigProperties getProperties() { + @Override + public OpenTelemetrySdk getOpenTelemetrySdk() { + return sdk; + } + + @Override + Resource getResource() { + return Resource.getDefault(); + } + + @Override + public ConfigProperties getConfig() { return properties; } + @Override + Object getConfigProvider() { + return null; + } + @Override public boolean supportsParameter( ParameterContext parameterContext, ExtensionContext extensionContext) { @@ -117,6 +147,7 @@ public static class Builder { private final SdkCustomizer customizer = new SdkCustomizer(); private final Map properties = new HashMap<>(); private final List propagators = new ArrayList<>(); + private final List agentListeners = new ArrayList<>(); private Sampler sampler = Sampler.alwaysOn(); public Builder withProperty(String name, String value) { @@ -139,6 +170,11 @@ public Builder with(TextMapPropagator propagator) { return this; } + public Builder with(AgentListener agentListener) { + this.agentListeners.add(agentListener); + return this; + } + /** * Simplified re-implementation of AutoConfiguredOpenTelemetrySdkBuilder's build method. The * OpenTelemetry SDK is only configured with features necessary to pass existing test use cases. @@ -155,7 +191,7 @@ public OpenTelemetrySdkExtension build() { .setTracerProvider(tracerProvider) .setPropagators(contextPropagators) .build(); - return new OpenTelemetrySdkExtension(sdk, configProperties); + return new OpenTelemetrySdkExtension(sdk, configProperties, agentListeners); } private ConfigProperties customizeProperties() {