diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/ExitTracer.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/ExitTracer.java index e72c22284a..5be047bb27 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/ExitTracer.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/ExitTracer.java @@ -7,6 +7,8 @@ package com.newrelic.agent.bridge; +import com.newrelic.api.agent.Token; + import java.lang.reflect.InvocationHandler; @@ -19,10 +21,19 @@ public interface ExitTracer extends InvocationHandler, TracedMethod { */ void finish(int opcode, Object returnValue); + default void finish() { + // 177 is Opcodes.RETURN + finish(177, null); + } + /** * Called when a method invocation throws an exception. * * @param throwable */ void finish(Throwable throwable); + + default Token getToken() { + return NoOpToken.INSTANCE; + } } diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/Instrumentation.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/Instrumentation.java index 907026fa75..a3593b7cae 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/Instrumentation.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/Instrumentation.java @@ -12,7 +12,6 @@ import com.newrelic.api.agent.NewRelic; import com.newrelic.api.agent.Token; -import com.newrelic.api.agent.Trace; public interface Instrumentation { @@ -59,6 +58,10 @@ ExitTracer createTracer(Object invocationTarget, int signatureId, boolean dispat ExitTracer createScalaTxnTracer(); + default ExitTracer createTracer(String metricName, int flags) { + return null; + } + /** * Returns the current transaction. This should not be called directly - instead use {@link Agent#getTransaction()}. * diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/TracedMethod.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/TracedMethod.java index c3b064dceb..3a5dbfd3d6 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/TracedMethod.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/TracedMethod.java @@ -16,6 +16,12 @@ */ public interface TracedMethod extends com.newrelic.api.agent.TracedMethod { + default String getTraceId() { + return "0000000000000000"; + } + default String getSpanId() { + return "0000000000000000"; + } /** * Returns the parent of this traced method, or null if this is the root tracer. * diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/datastore/SqlQueryConverter.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/datastore/SqlQueryConverter.java new file mode 100644 index 0000000000..ad8938f341 --- /dev/null +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/datastore/SqlQueryConverter.java @@ -0,0 +1,27 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.bridge.datastore; + +import com.newrelic.api.agent.QueryConverter; + +public final class SqlQueryConverter implements QueryConverter { + public static final QueryConverter INSTANCE = new SqlQueryConverter(); + + private SqlQueryConverter() { + } + + @Override + public String toRawQueryString(String rawQuery) { + return rawQuery; + } + + @Override + public String toObfuscatedQueryString(String rawQuery) { + return null; + } +} diff --git a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/SpanEvent.java b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/SpanEvent.java index 3e30b34fc4..fac4c19f82 100644 --- a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/SpanEvent.java +++ b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/SpanEvent.java @@ -10,6 +10,8 @@ import java.util.Map; public interface SpanEvent { + String getGuid(); + String getName(); float duration(); @@ -33,4 +35,6 @@ public interface SpanEvent { String getStatusText(); Map getAgentAttributes(); + + Map getUserAttributes(); } diff --git a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/SpanEventImpl.java b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/SpanEventImpl.java index fcaa743ee5..922b6d5552 100644 --- a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/SpanEventImpl.java +++ b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/SpanEventImpl.java @@ -19,6 +19,16 @@ public SpanEventImpl(com.newrelic.agent.model.SpanEvent spanEvent) { this.spanEvent = spanEvent; } + /** + * This method is just to facilitate testing + * + * @return String representing Span ID + */ + @Override + public String getGuid() { + return spanEvent.getGuid(); + } + @Override public String getName() { return spanEvent.getName(); @@ -74,9 +84,13 @@ public String getStatusText() { return (String) spanEvent.getAgentAttributes().get("http.statusText"); } - @Override public Map getAgentAttributes() { return spanEvent.getAgentAttributes(); } + + @Override + public Map getUserAttributes() { + return spanEvent.getUserAttributesCopy(); + } } diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/README.md b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/README.md new file mode 100644 index 0000000000..b579846734 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/README.md @@ -0,0 +1,301 @@ +# OpenTelemetry Instrumentation + +This instrumentation module instruments parts of the OpenTelemetry SDK in order to incorporate signals (metrics, logs, and traces) emitted by OpenTelemetry APIs into the New Relic Java agent. + +Specifically, it can: + +* Detect when Spans are emitted by OpenTelemetry APIs and incorporate them to New Relic Java agent traces. +* Detect when LogRecords are emitted by OpenTelemetry APIs report them to the APM entity being monitored by the Java agent as New Relic LogEvents. +* Autoconfigure the OpenTelemetry SDK to export dimensional metrics (over OTLP) to the APM entity being monitored by the Java agent. + +## OpenTelemetry Requirements + +The [opentelemetry-sdk-extension-autoconfigure](https://central.sonatype.com/artifact/io.opentelemetry/opentelemetry-sdk-extension-autoconfigure) dependency (version 1.28.0 or later) must be present in the application being monitored for this instrumentation module to apply. The [opentelemetry-exporter-otlp](https://central.sonatype.com/artifact/io.opentelemetry/opentelemetry-exporter-otlp) dependency (version 1.28.0 or later) must also be present for dimensional metrics to be exported to New Relic. + +```groovy +implementation(platform("io.opentelemetry:opentelemetry-bom:1.44.1")) +implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") +implementation("io.opentelemetry:opentelemetry-exporter-otlp") +``` + +Additionally, automatic configuration of the OpenTelemetry SDK must be enabled by one of the following options: + +System property: +```commandline +-Dotel.java.global-autoconfigure.enabled=true +``` + +Environment variable: +```commandline +export OTEL_JAVA_GLOBAL_AUTOCONFIGURE_ENABLED=true +``` + +Programmatic: +```java +/* + * The opentelemetry-sdk-extension-autoconfigure dependency needs to be initialized + * by either setting -Dotel.java.global-autoconfigure.enabled=true or calling the + * following API in order for the New Relic Java agent instrumentation to load. + */ +private static final OpenTelemetry OPEN_TELEMETRY_SDK = AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk(); +``` + +## New Relic Java Agent Configuration + +Telemetry signals (Logs, Metrics, and Traces) emitted by OpenTelemetry APIs can be incorporated into the Java agent and controlled by the following config options. + +Configuration via YAML: + +```yaml + # Telemetry signals (Logs, Metrics, and Traces) emitted by OpenTelemetry APIs can + # be incorporated into the Java agent and controlled by the following config options. + opentelemetry: + + # Set to true to allow individual OpenTelemetry signals to be enabled, false to disable all OpenTelemetry signals. + # Default is false. + enabled: true + + # OpenTelemetry Logs signals. + logs: + + # Set to true to enable OpenTelemetry Logs signals. + # Default is false. + enabled: true + + # OpenTelemetry Metrics signals. + metrics: + + # Set to true to enable OpenTelemetry Metrics signals. + # Default is false. + enabled: true + + # A comma-delimited string of OpenTelemetry Meters (e.g. "MeterName1,MeterName2") whose signals should be included. + # By default, all Meters are included. This will override any default Meter excludes in the agent, effectively re-enabling them. + include: "MeterName1,MeterName2" + + # A comma-delimited string of OpenTelemetry Meters (e.g. "MeterName3,MeterName4") whose signals should be excluded. + # This takes precedence over all other includes/excludes sources, effectively disabling the listed Meters. + exclude: "MeterName3,MeterName4" + + # OpenTelemetry Traces signals. + traces: + + # Set to true to enable OpenTelemetry Traces signals. + # Default is false. + enabled: true + + # A comma-delimited string of OpenTelemetry Tracers (e.g. "TracerName1,TracerName2") whose signals should be included. + # By default, all Tracers are included. This will override any default Tracer excludes in the agent, effectively re-enabling them. + include: "TracerName1,TracerName2" + + # A comma-delimited string of OpenTelemetry Tracers (e.g. "TracerName3,TracerName4") whose signals should be excluded. + # This takes precedence over all other includes/excludes sources, effectively disabling the listed Tracers. + exclude: "TracerName3,TracerName4" +``` + +Configuration via system property: + +``` +-Dnewrelic.config.opentelemetry.enabled=true + +-Dnewrelic.config.opentelemetry.logs.enabled=true + +-Dnewrelic.config.opentelemetry.metrics.enabled=true +-Dnewrelic.config.opentelemetry.metrics.include=MeterName1,MeterName2 +-Dnewrelic.config.opentelemetry.metrics.exclude=MeterName3,MeterName4 + +-Dnewrelic.config.opentelemetry.traces.enabled=true +-Dnewrelic.config.opentelemetry.traces.include=TracerName1,TracerName2 +-Dnewrelic.config.opentelemetry.traces.exclude=TracerName3,TracerName4 +``` + +Configuration via environment variable: + +``` +NEW_RELIC_OPENTELEMETRY_ENABLED=true + +NEW_RELIC_OPENTELEMETRY_LOGS_ENABLED=true + +NEW_RELIC_OPENTELEMETRY_METRICS_ENABLED=true +NEW_RELIC_OPENTELEMETRY_METRICS_INCLUDE=MeterName1,MeterName2 +NEW_RELIC_OPENTELEMETRY_METRICS_EXCLUDE=MeterName3,MeterName4 + +NEW_RELIC_OPENTELEMETRY_TRACES_ENABLED=true +NEW_RELIC_OPENTELEMETRY_TRACES_INCLUDE=TracerName1,TracerName2 +NEW_RELIC_OPENTELEMETRY_TRACES_EXCLUDE=TracerName3,TracerName4 +``` + +### Deprecated Config + +The following deprecated config option should no longer be used and is only kept for backwards compatibility. + +This config was originally used to enable/disable metrics signals and has been replaced by `opentelemetry.metrics.enabled`: + +```yaml + opentelemetry: + sdk: + autoconfigure: + enabled: true +``` + +## OpenTelemetry Dimensional Metrics Signals + +The [OpenTelemetry Metrics API](https://opentelemetry.io/docs/specs/otel/metrics/api/) can be used to create dimensional metrics which will be exported by the OpenTelemetry SDK to New Relic over OTLP. The dimensional metrics will be decorated with the `entity.guid` of the APM entity being monitored by the New Relic Java agent. + +Example of OpenTelemetry APIs being used to record dimensional metrics: + +```java + // Generate LongCounter dimensional metrics + LongCounter longCounter = GlobalOpenTelemetry.get() + .getMeterProvider() + .get("opentelemetry-metrics-api-demo") + .counterBuilder("opentelemetry-metrics-api-demo.longcounter") + .build(); + longCounter.add(1, Attributes.of(AttributeKey.stringKey("LongCounter"), "foo")); + + // Generate DoubleHistogram dimensional metrics + DoubleHistogram doubleHistogram = GlobalOpenTelemetry.get() + .getMeterProvider() + .get("opentelemetry-metrics-api-demo") + .histogramBuilder("opentelemetry-metrics-api-demo.histogram") + .build(); + doubleHistogram.record(3, Attributes.of(AttributeKey.stringKey("DoubleHistogram"), "foo")); + + // Generate DoubleGauge dimensional metrics + DoubleGauge doubleGauge = GlobalOpenTelemetry.get() + .getMeterProvider() + .get("opentelemetry-metrics-api-demo") + .gaugeBuilder("opentelemetry-metrics-api-demo.gauge") + .build(); + doubleGauge.set(5, Attributes.of(AttributeKey.stringKey("DoubleGauge"), "foo")); + + // Generate LongUpDownCounter dimensional metrics + LongUpDownCounter longUpDownCounter = GlobalOpenTelemetry.get() + .getMeterProvider() + .get("opentelemetry-metrics-api-demo") + .upDownCounterBuilder("opentelemetry-metrics-api-demo.updowncounter") + .build(); + longUpDownCounter.add(7, Attributes.of(AttributeKey.stringKey("LongUpDownCounter"), "foo")); +``` + +Any recorded dimensional metrics can be found in the Metrics Explorer for the associated APM entity and can be used to build custom dashboards. + +## OpenTelemetry Traces Signals + +Documented below are several approaches for incorporating OpenTelemetry Traces (aka Spans) into New Relic Java agent traces. + +### `@WithSpan` Annotation + +The New Relic Java agent will detect usage of the OpenTelemetry [@WithSpan](https://opentelemetry.io/docs/zero-code/java/agent/annotations/) annotation. The +`@WithSpan` annotation can be used as an alternative to the `@Trace` annotation. + +This does not currently support the following config options: + +* [Suppressing @WithSpan instrumentation](https://opentelemetry.io/docs/zero-code/java/agent/annotations/#suppressing-withspan-instrumentation) +* [Creating spans around methods with otel.instrumentation.methods.include](https://opentelemetry.io/docs/zero-code/java/agent/annotations/#creating-spans-around-methods-with-otelinstrumentationmethodsinclude) + +Note that OpenTelemetry config properties can be set through environment or system properties, like our agent, and eventually through a config file. We can use +our existing OpenTelemetry instrumentation model to get access to the normalized version of the instrumentation settings to include and exclude methods and pass +those to the core agent through the bridge. + +See `ClassTransformerConfigImpl.java` for implementation details of the `@WithSpan` annotation. + +### Spans Emitted From OpenTelemetry Tracing API + +The New Relic Java agent will detect Spans emitted by the [OpenTelemetry Tracing API](https://opentelemetry.io/docs/specs/otel/trace/api/) for most manual, library, and native [instrumentation types](https://opentelemetry.io/docs/languages/java/instrumentation/#instrumentation-categories) and incorporate them into New Relic traces. + +It does this by weaving the `io.opentelemetry.sdk.trace.SdkTracerProvider` so that it will create a New Relic Tracer each time an OpenTelemetry Span is started and +weaving the `io.opentelemetry.context.Context` to propagate context between New Relic and OpenTelemetry Spans. + +#### Translating OpenTelemetry SpanKinds To The New Relic Span Data Model + +Depending on the OpenTelemetry Span `SpanKind`, it may result in the New Relic Java agent starting a transaction (when one doesn't already exist). Also see [attribute-mappings.json](src/main/resources/attribute-mappings.json) for the complete attribute mapping rules. + +* `SpanKind.INTERNAL` + * Creating a span with no `SpanKind`, which defaults to `SpanKind.INTERNAL`, will not start a transaction + * If `SpanKind.INTERNAL` spans occur within an already existing New Relic transaction they will be included in the trace +* `SpanKind.CLIENT` + * Creating a span with `SpanKind.CLIENT` will not start a transaction. If a `CLIENT` span has certain db attributes it will be treated as a DB span, and + other specific attributes will cause it to be treated as an external span + * If `SpanKind.CLIENT` spans occur within an already existing New Relic transaction they will be included in the trace +* `SpanKind.SERVER` + * Creating a span with `SpanKind.SERVER` will start a `WebTransaction/Uri/*` transaction. + * If `SpanKind.SERVER` spans occur within an already existing New Relic transaction they will be included in the trace +* `SpanKind.CONSUMER` + * Creating a span with `SpanKind.CONSUMER` will start a `OtherTransaction/*` transaction. + * If `SpanKind.CONSUMER` spans occur within an already existing New Relic transaction they will be included in the trace +* `SpanKind.PRODUCER` + * Creating a span with `SpanKind.PRODUCER` will not start a transaction. There is no explicit processing for `PRODUCER` spans currently. + * If `SpanKind.PRODUCER` spans occur within an already existing New Relic transaction they will be included in the trace (though it's effectively no + different from a `SpanKind.INTERNAL` span) + +## OpenTelemetry Logs Signals + +The New Relic Java agent will detect LogRecords emitted by the [OpenTelemetry Logs API](https://opentelemetry.io/docs/specs/otel/logs/api/) and incorporate them into New Relic log events associated with the APM entity being monitored. The logs will be associated with a New Relic transaction if the logging occurred within one. + +Note: It is recommended that OpenTelemetry instrumentation for a particular logging framework (e.g. Logback, Log4j) should not be used alongside New Relic Java agent instrumentation for the same logging framework. Doing so could result in duplicate log events being reported to New Relic. + +Example usage of OpenTelemetry Logs APIs: + +```java + // create LogRecordExporter +private static final SystemOutLogRecordExporter systemOutLogRecordExporter = SystemOutLogRecordExporter.create(); + +// create LogRecordProcessor +private static final LogRecordProcessor logRecordProcessor = SimpleLogRecordProcessor.create(systemOutLogRecordExporter); + +// create Attributes +private static final Attributes attributes = Attributes.builder() + .put("service.name", NewRelic.getAgent().getConfig().getValue("app_name", "unknown")) + .put("service.version", "4.5.1") + .put("environment", "production") + .build(); + +// create Resource +private static final Resource customResource = Resource.create(attributes); + +// create SdkLoggerProvider +private static final SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder() + .addLogRecordProcessor(logRecordProcessor) + .setResource(customResource) + .build(); + +// create LoggerBuilder +private static final LoggerBuilder loggerBuilder = sdkLoggerProvider + .loggerBuilder("demo-otel-logger") + .setInstrumentationVersion("1.0.0") + .setSchemaUrl("https://opentelemetry.io/schemas/1.0.0"); + +// create Logger +private static final Logger logger = loggerBuilder.build(); + +// utility method to build and emit OpenTelemetry log records +public static void emitOTelLogs(Severity severity) { + // create LogRecordBuilder + LogRecordBuilder logRecordBuilder = logger.logRecordBuilder(); + + Instant now = Instant.now(); + logRecordBuilder +// .setContext() + .setBody("Generating OpenTelemetry LogRecord") + .setSeverity(severity) + .setSeverityText("This is the severity text") + .setAttribute(AttributeKey.stringKey("foo"), "bar") + .setObservedTimestamp(now) + .setObservedTimestamp(now.toEpochMilli(), java.util.concurrent.TimeUnit.MILLISECONDS) + .setTimestamp(now) + .setTimestamp(now.toEpochMilli(), java.util.concurrent.TimeUnit.MILLISECONDS); + + if (severity == Severity.ERROR) { + try { + throw new RuntimeException("This is a test exception for severity ERROR"); + } catch (RuntimeException e) { + logRecordBuilder.setAttribute(AttributeKey.stringKey("exception.message"), e.getMessage()); + logRecordBuilder.setAttribute(AttributeKey.stringKey("exception.type"), e.getClass().getName()); + logRecordBuilder.setAttribute(AttributeKey.stringKey("exception.stacktrace"), Arrays.toString(e.getStackTrace())); + } + } + + logRecordBuilder.emit(); +} +``` \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/build.gradle b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/build.gradle index b0964a733c..16cc88e984 100644 --- a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/build.gradle +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/build.gradle @@ -1,18 +1,67 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + plugins { id("java") } +configurations { + shadowIntoJar +} +configurations.implementation.extendsFrom(configurations.shadowIntoJar) + dependencies { + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.28.0") implementation(project(":agent-bridge")) implementation(project(":newrelic-api")) implementation(project(":newrelic-weaver-api")) - implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.28.0") + implementation(project(":newrelic-agent")) + + shadowIntoJar('com.googlecode.json-simple:json-simple:1.1.1') { + transitive = false + } + testImplementation("junit:junit:4.12") + testImplementation("io.opentelemetry:opentelemetry-exporter-otlp:1.28.0") + testImplementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:1.28.0") + testImplementation("com.google.guava:guava:30.1.1-jre") + testImplementation("org.mockito:mockito-inline:4.11.0") } +/** + * We have to shadow dependencies into instrumentation modules + * so they're accessible. We try not to rely on the agent dependencies + * otherwise. + */ +tasks.create("shadowJar", ShadowJar) { + archiveClassifier.set("shadowed") + setConfigurations([project.configurations.shadowIntoJar]) + from(sourceSets.main.output.classesDirs) + relocate("org.json.simple", "com.nr.agent.deps.org.json.simple") +} + +artifacts { + instrumentationWithDependencies shadowJar +} + +project.tasks.getByName("writeCachedWeaveAttributes").dependsOn(shadowJar) + +/** + * shadowJar takes care of dependencies, but the jar task is what + * the agent build wants, so we copy the shadowJar contents. + */ jar { + dependsOn("shadowJar") + from(zipTree(project.tasks["shadowJar"].archiveFile.get().asFile.path)) + + // The default jar task re-includes the original classes files, which we don't want. + exclude { + sourceSets.main.output.classesDirs.any { dir -> + it.getFile().getPath().startsWith(dir.getPath()) + } + } + manifest { - attributes "Implementation-Title" : "com.newrelic.instrumentation.opentelemetry-sdk-extension-autoconfigure-1.28.0" + attributes "Implementation-Title": "com.newrelic.instrumentation.opentelemetry-sdk-extension-autoconfigure-1.28.0", "Implementation-Title-Alias": "opentelemetry_instrumentation" } } diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/AttributesHelper.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/AttributesHelper.java new file mode 100644 index 0000000000..c82bdc208c --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/AttributesHelper.java @@ -0,0 +1,35 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.utils; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +import java.util.Map; + +/** + * Helper class for turning a map of attributes into an OTel Attributes object. + */ +public class AttributesHelper { + private AttributesHelper() { + } + + public static Attributes toAttributes(Map attributes) { + AttributesBuilder builder = Attributes.builder(); + attributes.forEach((key, value) -> { + if (value instanceof String) { + builder.put(key, (String) value); + } else if (value instanceof Float || value instanceof Double) { + builder.put(key, ((Number) value).doubleValue()); + } else if (value instanceof Number) { + builder.put(key, ((Number) value).longValue()); + } + }); + return builder.build(); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/config/OpenTelemetryConfig.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/config/OpenTelemetryConfig.java new file mode 100644 index 0000000000..5f1b7cf2f1 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/config/OpenTelemetryConfig.java @@ -0,0 +1,146 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.utils.config; + +import com.newrelic.api.agent.NewRelic; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; + +/** + * Configuration for the OpenTelemetry hybrid agent functionality. + * This config controls which, if any, of the opentelemetry logs, metrics, and traces signals will be captured. + */ +public class OpenTelemetryConfig { + // We don't have any default excludes, but they should be added here if needed in the future + public static final Set DEFAULT_METER_EXCLUDES = new HashSet<>(); + public static final Set DEFAULT_TRACER_EXCLUDES = new HashSet<>(); + + public static final String COMMA_SEPARATOR = ","; + + public static final String OPENTELEMETRY_ENABLED = "opentelemetry.enabled"; + public static final Boolean OPENTELEMETRY_ENABLED_DEFAULT = false; + public static final String OPENTELEMETRY_LOGS_ENABLED = "opentelemetry.logs.enabled"; + public static final Boolean OPENTELEMETRY_LOGS_ENABLED_DEFAULT = false; + public static final String OPENTELEMETRY_METRICS_ENABLED = "opentelemetry.metrics.enabled"; + public static final Boolean OPENTELEMETRY_METRICS_ENABLED_DEFAULT = false; + public static final String OPENTELEMETRY_TRACES_ENABLED = "opentelemetry.traces.enabled"; + public static final Boolean OPENTELEMETRY_TRACES_ENABLED_DEFAULT = false; + public static final String OPENTELEMETRY_SDK_AUTOCONFIGURE_ENABLED = "opentelemetry.sdk.autoconfigure.enabled"; + public static final Boolean OPENTELEMETRY_SDK_AUTOCONFIGURE_ENABLED_DEFAULT = OPENTELEMETRY_METRICS_ENABLED_DEFAULT; + + public static final String OPENTELEMETRY_METRICS_EXCLUDE = "opentelemetry.metrics.exclude"; + public static final String OPENTELEMETRY_METRICS_INCLUDE = "opentelemetry.metrics.include"; + public static final String OPENTELEMETRY_TRACES_EXCLUDE = "opentelemetry.traces.exclude"; + public static final String OPENTELEMETRY_TRACES_INCLUDE = "opentelemetry.traces.include"; + + public static boolean isOpenTelemetrySdkAutoConfigureEnabled() { + // Legacy setting that was only used to enable SDK exporting of OTel metrics. Kept for backwards compatability. This functioned the same as opentelemetry.enabled now does. + return NewRelic.getAgent().getConfig().getValue(OPENTELEMETRY_SDK_AUTOCONFIGURE_ENABLED, OPENTELEMETRY_SDK_AUTOCONFIGURE_ENABLED_DEFAULT); + } + + public static boolean isOpenTelemetryEnabled() { + return NewRelic.getAgent().getConfig().getValue(OPENTELEMETRY_ENABLED, OPENTELEMETRY_ENABLED_DEFAULT); + } + + public static boolean isOpenTelemetryLogsEnabled() { + return isOpenTelemetryEnabled() && NewRelic.getAgent().getConfig().getValue(OPENTELEMETRY_LOGS_ENABLED, OPENTELEMETRY_LOGS_ENABLED_DEFAULT); + } + + public static boolean isOpenTelemetryMetricsEnabled() { + return isOpenTelemetryEnabled() && NewRelic.getAgent().getConfig().getValue(OPENTELEMETRY_METRICS_ENABLED, OPENTELEMETRY_METRICS_ENABLED_DEFAULT); + } + + public static boolean isOpenTelemetryTracesEnabled() { + return isOpenTelemetryEnabled() && NewRelic.getAgent().getConfig().getValue(OPENTELEMETRY_TRACES_ENABLED, OPENTELEMETRY_TRACES_ENABLED_DEFAULT); + } + + /** + * Creates a combined list of excludes based on the precedence of: + * user excludes > user includes > default excludes + * + * @return a final combined list of excluded OpenTelemetry Meters + */ + public static List getOpenTelemetryMetricsExcludes() { + NewRelic.getAgent().getLogger().log(Level.INFO, "Default excluded OpenTelemetry meters: " + DEFAULT_METER_EXCLUDES); + + // User configured includes take precedence over default excludes, so we filter them out. + getOpenTelemetryMetricsIncludes().forEach(DEFAULT_METER_EXCLUDES::remove); + + // Combine the remaining default excludes with the user configured excludes for the final excludes list. + String metricsExclude = NewRelic.getAgent().getConfig().getValue(OPENTELEMETRY_METRICS_EXCLUDE, ""); + List excludedMeters = getUniqueStringsFromString(metricsExclude, COMMA_SEPARATOR); + excludedMeters.addAll(DEFAULT_METER_EXCLUDES); + return excludedMeters; + } + + public static List getOpenTelemetryMetricsIncludes() { + String metricsInclude = NewRelic.getAgent().getConfig().getValue(OPENTELEMETRY_METRICS_INCLUDE, ""); + return getUniqueStringsFromString(metricsInclude, COMMA_SEPARATOR); + } + + /** + * Creates a combined list of excludes based on the precedence of: + * user excludes > user includes > default excludes + * + * @return a final combined list of excluded OpenTelemetry Tracers + */ + public static List getOpenTelemetryTracesExcludes() { + NewRelic.getAgent().getLogger().log(Level.INFO, "Default excluded OpenTelemetry tracers: " + DEFAULT_TRACER_EXCLUDES); + + // User configured includes take precedence over default excludes, so we filter them out. + getOpenTelemetryTracesIncludes().forEach(DEFAULT_TRACER_EXCLUDES::remove); + + // Combine the remaining default excludes with the user configured excludes for the final excludes list. + String tracesExclude = NewRelic.getAgent().getConfig().getValue(OPENTELEMETRY_TRACES_EXCLUDE, ""); + List excludedTracers = getUniqueStringsFromString(tracesExclude, COMMA_SEPARATOR); + excludedTracers.addAll(DEFAULT_TRACER_EXCLUDES); + return excludedTracers; + } + + public static List getOpenTelemetryTracesIncludes() { + String tracesInclude = NewRelic.getAgent().getConfig().getValue(OPENTELEMETRY_TRACES_INCLUDE, ""); + return getUniqueStringsFromString(tracesInclude, COMMA_SEPARATOR); + } + + /** + * Checks if the provided instrumentationScopeName is in the list of excluded Tracers. + * + * @param instrumentationScopeName Name of the OpenTelemetry instrumentation + * @return true if the Tracer is disabled, false if enabled + */ + public static boolean isOpenTelemetryTracerDisabled(String instrumentationScopeName) { + List openTelemetryTracesExcludes = getOpenTelemetryTracesExcludes(); + return openTelemetryTracesExcludes.contains(instrumentationScopeName); + } + + /** + * Splits the given values String into a collection of Strings based on the provided separator character. + * + * @param valuesString A separator delimited string + * @param separator A character delimiter in a string + * @return List of string split by the separator delimiter + */ + public static List getUniqueStringsFromString(String valuesString, String separator) { + List result = new ArrayList<>(); + if (valuesString == null || valuesString.isEmpty()) { + return result; + } + String[] valuesArray = valuesString.split(separator); + for (String value : valuesArray) { + value = value.trim(); + if (!value.isEmpty() && !result.contains(value)) { + result.add(value); + } + } + return result; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/header/HeaderType.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/header/HeaderType.java new file mode 100644 index 0000000000..432cda42bd --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/header/HeaderType.java @@ -0,0 +1,14 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.utils.header; + +public class HeaderType { + public static final String NEWRELIC = "newrelic"; + public static final String W3C_TRACEPARENT = "traceparent"; + public static final String W3C_TRACESTATE = "tracestate"; +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/header/W3CTraceParentHeader.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/header/W3CTraceParentHeader.java new file mode 100644 index 0000000000..bc1b1962fd --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/header/W3CTraceParentHeader.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.utils.header; + +import io.opentelemetry.api.trace.SpanContext; + +public class W3CTraceParentHeader { + + static final String W3C_VERSION = "00"; + static final String W3C_TRACE_PARENT_DELIMITER = "-"; + + public static String create(SpanContext parentSpanContext) { + final String traceId = parentSpanContext.getTraceId(); + final String spanId = parentSpanContext.getSpanId(); + final boolean sampled = parentSpanContext.isSampled(); + + String traceParentHeader = + W3C_VERSION + W3C_TRACE_PARENT_DELIMITER + traceId + W3C_TRACE_PARENT_DELIMITER + spanId + W3C_TRACE_PARENT_DELIMITER + sampledToFlags(sampled); + + boolean valid = W3CTraceParentValidator.forHeader(traceParentHeader) + .version(W3C_VERSION) + .traceId(traceId) + .parentId(spanId) + .flags(parentSpanContext.getTraceFlags().asHex()) + .isValid(); + + return valid ? traceParentHeader : ""; + } + + private static String sampledToFlags(boolean sampled) { + return sampled ? "01" : "00"; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/header/W3CTraceParentValidator.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/header/W3CTraceParentValidator.java new file mode 100644 index 0000000000..0d627c8e9e --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/header/W3CTraceParentValidator.java @@ -0,0 +1,126 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.utils.header; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.nr.agent.instrumentation.utils.header.W3CTraceParentHeader.W3C_VERSION; +import static java.util.regex.Pattern.compile; + +public class W3CTraceParentValidator { + + private static final String INVALID_VERSION = "ff"; + private static final String INVALID_TRACE_ID = "00000000000000000000000000000000"; // 32 characters + private static final String INVALID_PARENT_ID = "0000000000000000"; // 16 characters + private static final Pattern HEXADECIMAL_PATTERN = compile("\\p{XDigit}+"); + + private final String traceParentHeader; + private final String version; + private final String traceId; + private final String parentId; + private final String flags; + + private W3CTraceParentValidator(Builder builder) { + this.traceParentHeader = builder.traceParentHeader; + this.version = builder.version; + this.traceId = builder.traceId; + this.parentId = builder.parentId; + this.flags = builder.flags; + } + + private boolean isValid() { + return isValidVersion() && isValidTraceId() && isValidParentId() && isValidFlags(); + } + + /** + * Version can only be 2 hexadecimal characters, `ff` is not allowed and if it matches our expected version the length must be 55 characters + */ + boolean isValidVersion() { + return version.length() == 2 && isHexadecimal(version.charAt(0)) && isHexadecimal(version.charAt(1)) && !version.equals(INVALID_VERSION) && + !(version.equals(W3C_VERSION) && traceParentHeaderLengthIsInvalid()); + } + + private boolean traceParentHeaderLengthIsInvalid() { + return traceParentHeader.length() != 55; + } + + boolean isHexadecimal(char character) { + return Character.digit(character, 16) != -1; + } + + boolean isHexadecimal(String input) { + final Matcher matcher = HEXADECIMAL_PATTERN.matcher(input); + return matcher.matches(); + } + + /** + * TraceId must be 32 characters, not all zeros and must be hexadecimal + */ + boolean isValidTraceId() { + return traceId.length() == 32 && !traceId.equals(INVALID_TRACE_ID) && isHexadecimal(traceId); + } + + /** + * ParentId must be 16 characters, not all zeros and must be hexadecimal + */ + boolean isValidParentId() { + return parentId.length() == 16 && !parentId.equals(INVALID_PARENT_ID) && isHexadecimal(parentId); + } + + /** + * Flags must be 2 characters and must be hexadecimal + */ + boolean isValidFlags() { + return flags.length() == 2 && isHexadecimal(flags); + } + + static Builder forHeader(String traceParentHeader) { + return new Builder(traceParentHeader); + } + + static class Builder { + private final String traceParentHeader; + private String version; + private String traceId; + private String parentId; + private String flags; + + Builder(String traceParentHeader) { + this.traceParentHeader = traceParentHeader; + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder traceId(String traceId) { + this.traceId = traceId; + return this; + } + + public Builder parentId(String parentId) { + this.parentId = parentId; + return this; + } + + public Builder flags(String flags) { + this.flags = flags; + return this; + } + + W3CTraceParentValidator build() { + return new W3CTraceParentValidator(this); + } + + public boolean isValid() { + return build().isValid(); + } + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/logs/ExceptionUtil.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/logs/ExceptionUtil.java new file mode 100644 index 0000000000..0f9ca3ddab --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/logs/ExceptionUtil.java @@ -0,0 +1,37 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.utils.logs; + +public class ExceptionUtil { + public static final int MAX_STACK_SIZE = 300; + + public static String getErrorStack(String errorStack) { + if (validateString(errorStack) == null) { + return null; + } + if (errorStack.length() <= MAX_STACK_SIZE) { + return errorStack; + } + return errorStack.substring(0, MAX_STACK_SIZE); + } + + public static String getErrorMessage(String errorMessage) { + return validateString(errorMessage); + } + + public static String getErrorClass(String errorClass) { + return validateString(errorClass); + } + + public static String validateString(String s) { + if (s == null || s.isEmpty()) { + return null; + } + return s; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/logs/LogDuplicationChecker.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/logs/LogDuplicationChecker.java new file mode 100644 index 0000000000..9681b4d51d --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/logs/LogDuplicationChecker.java @@ -0,0 +1,149 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.utils.logs; + +import com.newrelic.api.agent.NewRelic; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +/** + * Utility class to help prevent duplicate log events when both OpenTelemetry logging framework + * instrumentation and New Relic weave instrumentation of the same logging framework are enabled. + */ +public class LogDuplicationChecker { + // These are known classes from standalone OpenTelemetry instrumentation of logging frameworks that we also weave. + private static final String LOGBACK_OTEL_INSTRUMENTATION_CLASS = "io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"; + private static final String LOG4J_OTEL_INSTRUMENTATION_CLASS = "io.opentelemetry.instrumentation.log4j.appender.v2_17.OpenTelemetryAppender"; + + // Static flags to indicate if known OpenTelemetry log framework instrumentation is present on the classpath. + private static final AtomicBoolean logbackOTelInstrumentationInstalled = isOTelLogFrameworkInstrumentationInstalled(LOGBACK_OTEL_INSTRUMENTATION_CLASS); + private static final AtomicBoolean log4jOTelInstrumentationInstalled = isOTelLogFrameworkInstrumentationInstalled(LOG4J_OTEL_INSTRUMENTATION_CLASS); + + // These are the names of our weave instrumentation modules for logging frameworks that OTel also instruments. + private static final String LOGBACK_WEAVE_INSTRUMENTATION_NAME = "logback-classic-1.2"; + private static final String LOG4J_WEAVE_INSTRUMENTATION_NAME = "apache-log4j-2.11"; + + // Static flags to indicate if we have weave instrumentation enabled for the corresponding logging framework. + private static final AtomicBoolean logbackWeaveInstrumentationEnabled = isLogFrameworkWeaveInstrumentationEnabled(LOGBACK_WEAVE_INSTRUMENTATION_NAME); + private static final AtomicBoolean log4jWeaveInstrumentationEnabled = isLogFrameworkWeaveInstrumentationEnabled(LOG4J_WEAVE_INSTRUMENTATION_NAME); + + /** + * Check if a specific OpenTelemetry log framework instrumentation class is present on the classpath. + * + * @param otelInstrumentationClass to check (e.g. io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender) + * @return AtomicBoolean true if the class is present, otherwise false + */ + private static AtomicBoolean isOTelLogFrameworkInstrumentationInstalled(String otelInstrumentationClass) { + try { + Class.forName(otelInstrumentationClass); + } catch (ClassNotFoundException __) { + return new AtomicBoolean(false); + } + NewRelic.getAgent().getLogger().log(Level.WARNING, "Detected " + otelInstrumentationClass + + " on the classpath. This may result in duplicate log events if the corresponding New Relic logging framework instrumentation module is also enabled"); + return new AtomicBoolean(true); + } + + /** + * Check if a weave instrumentation module is enabled for a specific logging framework. + * + * @param weaveModuleName to check (e.g. logback-classic-1.2, apache-log4j-2.11) + * @return AtomicBoolean true if the weave module is enabled, otherwise false + */ + private static AtomicBoolean isLogFrameworkWeaveInstrumentationEnabled(String weaveModuleName) { + final boolean weaveModuleEnabled = NewRelic.getAgent() + .getConfig() + .getValue("class_transformer.com.newrelic.instrumentation." + weaveModuleName + ".enabled", true); + return new AtomicBoolean(weaveModuleEnabled); + } + + /** + * Inspect the stack trace to determine if a log originated from a known OpenTelemetry + * log framework instrumentation class. + * + * @param otelInstrumentationClass known OTel log framework instrumentation class to search for in the stack trace + * @return true if the log originated from the specified OTel log framework instrumentation, otherwise false + */ + private static boolean isLogFromOTelLogFrameworkInstrumentation(String otelInstrumentationClass) { + StackTraceElement[] stackTrace = new Throwable().getStackTrace(); + for (StackTraceElement element : stackTrace) { + if (element.getClassName().toLowerCase().contains(otelInstrumentationClass.toLowerCase())) { + return true; + } + } + return false; + } + + /** + * Log a message indicating that a LogEvent will not be created from OpenTelemetry + * log framework instrumentation to avoid duplication with the corresponding New Relic + * weave instrumentation module. + * + * @param otelInstrumentationClass known OTel log framework instrumentation class (e.g. io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender) + * @param weaveInstrumentationName corresponding New Relic weave instrumentation module name (e.g. logback-classic-1.2) + */ + private static void logEventSkippedMessage(String otelInstrumentationClass, String weaveInstrumentationName) { + NewRelic.getAgent() + .getLogger() + .log(Level.FINEST, + "Skipped creating a LogEvent from " + otelInstrumentationClass + " to avoid duplication with the " + + weaveInstrumentationName + " instrumentation module. The " + weaveInstrumentationName + + " instrumentation module must be disabled to defer to the OpenTelemetry instrumentation."); + + } + + /** + * Determine if a LogEvent should be created from an OpenTelemetry log based + * on inspecting the stack trace to determine if it originated from known + * OTel of a logging framework that we also weave. + * + * @return true if a LogEvent should be created, otherwise false + */ + public static boolean shouldRecordLogFromOTelAPI() { + // No OTel log framework instrumentation is installed, so duplication is not a concern. + // Allow the LogEvent to be created from the OTel API. + if (!logbackOTelInstrumentationInstalled.get() && !log4jOTelInstrumentationInstalled.get()) { + return true; + } + + // Check if OTel logback framework instrumentation is installed. + if (logbackOTelInstrumentationInstalled.get()) { + // OTel logback framework instrumentation is installed, so check if our corresponding weave instrumentation is disabled. + if (!logbackWeaveInstrumentationEnabled.get()) { + // Our weave instrumentation is disabled, so create the LogEvent from the OTel API. + return true; + } else { + // Our weave instrumentation is enabled, we need to potentially prevent duplicated logs. + if (isLogFromOTelLogFrameworkInstrumentation(LOGBACK_OTEL_INSTRUMENTATION_CLASS)) { + // If the log originated from OTel logback framework instrumentation, it should be skipped to avoid duplication. + logEventSkippedMessage(LOGBACK_OTEL_INSTRUMENTATION_CLASS, LOGBACK_WEAVE_INSTRUMENTATION_NAME); + return false; + } + } + } + + // Check if OTel log4j framework instrumentation is installed. + if (log4jOTelInstrumentationInstalled.get()) { + // OTel log4j framework instrumentation is installed, so check if our corresponding weave instrumentation is disabled. + if (!log4jWeaveInstrumentationEnabled.get()) { + // Our weave instrumentation is disabled, so create the LogEvent from the OTel API. + return true; + } else { + // Our weave instrumentation is enabled, we need to potentially prevent duplicated logs. + if (isLogFromOTelLogFrameworkInstrumentation(LOG4J_OTEL_INSTRUMENTATION_CLASS)) { + // If the log originated from OTel log4j framework instrumentation, it should be skipped to avoid duplication. + logEventSkippedMessage(LOG4J_OTEL_INSTRUMENTATION_CLASS, LOG4J_WEAVE_INSTRUMENTATION_NAME); + return false; + } + } + } + // If the log doesn't originate from a known OTel instrumentation source, record the LogEvent. + return true; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/logs/LogEventUtil.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/logs/LogEventUtil.java new file mode 100644 index 0000000000..113c0f3aef --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/logs/LogEventUtil.java @@ -0,0 +1,191 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.utils.logs; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.logging.LogAttributeKey; +import com.newrelic.agent.bridge.logging.LogAttributeType; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.sdk.logs.NRLogRecord; +import io.opentelemetry.sdk.logs.data.Body; +import io.opentelemetry.sdk.logs.data.LogRecordData; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.ERROR_CLASS; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.ERROR_MESSAGE; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.ERROR_STACK; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.INSTRUMENTATION; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.LEVEL; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.MESSAGE; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.THREAD_ID; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.THREAD_NAME; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.TIMESTAMP; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.UNKNOWN; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.isAppLoggingContextDataEnabled; +import static io.opentelemetry.sdk.logs.NRLogRecord.BasicLogRecordData; +import static io.opentelemetry.sdk.logs.NRLogRecord.OTEL_EXCEPTION_MESSAGE; +import static io.opentelemetry.sdk.logs.NRLogRecord.OTEL_EXCEPTION_STACKTRACE; +import static io.opentelemetry.sdk.logs.NRLogRecord.OTEL_EXCEPTION_TYPE; +import static io.opentelemetry.sdk.logs.NRLogRecord.OTEL_LIBRARY_NAME; +import static io.opentelemetry.sdk.logs.NRLogRecord.OTEL_LIBRARY_VERSION; +import static io.opentelemetry.sdk.logs.NRLogRecord.OTEL_SCOPE_NAME; +import static io.opentelemetry.sdk.logs.NRLogRecord.OTEL_SCOPE_VERSION; + +public class LogEventUtil { + private static final Set OTEL_ATTRIBUTES = new HashSet<>(Arrays.asList( + NRLogRecord.OTEL_EXCEPTION_MESSAGE.getKey(), + NRLogRecord.OTEL_EXCEPTION_TYPE.getKey(), + NRLogRecord.OTEL_EXCEPTION_STACKTRACE.getKey(), + NRLogRecord.THREAD_NAME.getKey(), + NRLogRecord.THREAD_ID_LONG.getKey(), + NRLogRecord.THREAD_ID_STRING.getKey()) + ); + + /** + * Record a LogEvent to be sent to New Relic. + * + * @param logRecordData to parse + */ + public static void recordNewRelicLogEvent(LogRecordData logRecordData) { + if (logRecordData != null) { + Body body = logRecordData.getBody(); + Attributes contextAttributes = logRecordData.getAttributes(); + String errorClass = contextAttributes.get(OTEL_EXCEPTION_TYPE); + String errorMessage = contextAttributes.get(OTEL_EXCEPTION_MESSAGE); + + if (shouldCreateLogEvent(body, errorClass, errorMessage)) { + // It is possible that logs are being emitted from OTel instrumentation of a logging framework that we also instrument (e.g. logback, log4j), which could lead to double reporting of LogEvents. We can prevent this by checking if the logs are coming from a known OTel instrumentation source and favoring our own framework instrumentation over it. + if (LogDuplicationChecker.shouldRecordLogFromOTelAPI()) { + Map logEventMap = new HashMap<>(calculateInitialMapSize(contextAttributes)); + logEventMap.put(INSTRUMENTATION, "opentelemetry-sdk-extension-autoconfigure-1.28.0"); + if (body != null && body.getType() == Body.Type.STRING) { + String bodyString = body.asString(); + if (bodyString != null && !bodyString.isEmpty()) { + logEventMap.put(MESSAGE, bodyString); + } + } + + // Use Timestamp if it is present, otherwise use ObservedTimestamp. + long timestampEpochNanos = logRecordData.getTimestampEpochNanos(); + if (timestampEpochNanos >= 0) { + logEventMap.put(TIMESTAMP, timestampEpochNanos); + } else { + logEventMap.put(TIMESTAMP, logRecordData.getObservedTimestampEpochNanos()); + } + + // otel.scope.version and otel.scope.name should be reported along with the deprecated versions otel.library.version and otel.library.name + String instrumentationScopeName = logRecordData.getInstrumentationScopeInfo().getName(); + + if (instrumentationScopeName != null && !instrumentationScopeName.isEmpty()) { + LogAttributeKey instrumentationScopeNameKey = new LogAttributeKey(OTEL_SCOPE_NAME.getKey(), LogAttributeType.AGENT); + logEventMap.put(instrumentationScopeNameKey, instrumentationScopeName); + + LogAttributeKey instrumentationLibraryNameKey = new LogAttributeKey(OTEL_LIBRARY_NAME.getKey(), LogAttributeType.AGENT); + logEventMap.put(instrumentationLibraryNameKey, instrumentationScopeName); + } + + String instrumentationScopeVersion = logRecordData.getInstrumentationScopeInfo().getVersion(); + + if (instrumentationScopeVersion != null && !instrumentationScopeVersion.isEmpty()) { + LogAttributeKey instrumentationScopeVersionKey = new LogAttributeKey(OTEL_SCOPE_VERSION.getKey(), LogAttributeType.AGENT); + logEventMap.put(instrumentationScopeVersionKey, instrumentationScopeVersion); + + LogAttributeKey instrumentationLibraryVersionKey = new LogAttributeKey(OTEL_LIBRARY_VERSION.getKey(), LogAttributeType.AGENT); + logEventMap.put(instrumentationLibraryVersionKey, instrumentationScopeVersion); + } + + if (isAppLoggingContextDataEnabled()) { + for (Map.Entry, Object> entry : contextAttributes.asMap().entrySet()) { + String key = entry.getKey().getKey(); + // Don't add the context prefix to OTel attributes that are already defined by the OTel spec. + if (!OTEL_ATTRIBUTES.contains(key)) { + String value = entry.getValue().toString(); + LogAttributeKey logAttrKey = new LogAttributeKey(key, LogAttributeType.CONTEXT); + logEventMap.put(logAttrKey, value); + } + } + } + + // These attributes come from the attribute map, but they should not be prefixed with context since they are defined in the OTel semantic conventions. + if (!contextAttributes.isEmpty()) { + // Exceptions are captured in the attributes map for OTel logs. + // https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-logs/ + if (ExceptionUtil.getErrorMessage(errorMessage) != null) { + logEventMap.put(ERROR_MESSAGE, errorMessage); + } + + if (ExceptionUtil.getErrorClass(errorClass) != null) { + logEventMap.put(ERROR_CLASS, errorClass); + } + + String errorStack = ExceptionUtil.getErrorStack(contextAttributes.get(OTEL_EXCEPTION_STACKTRACE)); + if (errorStack != null) { + logEventMap.put(ERROR_STACK, errorStack); + } + } + + Severity severity = logRecordData.getSeverity(); + if (severity != null) { + // Use SeverityText if it is present, otherwise use SeverityNumber and convert it to a textual representation based on the enum value. + String severityName = severity.toString(); + if (severityName == null || severityName.isEmpty()) { + int severityNumber = severity.getSeverityNumber(); + Severity[] severityValues = Severity.values(); + Severity severityValue = severityValues[severityNumber]; + severityName = severityValue.toString(); + // If we still don't have a valid severity name, set it to "UNKNOWN" + if (severityName == null || severityName.isEmpty()) { + logEventMap.put(LEVEL, UNKNOWN); + } else { + logEventMap.put(LEVEL, severityName); + } + } else { + logEventMap.put(LEVEL, severityName); + } + } + + String threadName = ((BasicLogRecordData) logRecordData).getThreadName(); + if (threadName != null) { + logEventMap.put(THREAD_NAME, threadName); + } + + long threadId = ((BasicLogRecordData) logRecordData).getThreadId(); + logEventMap.put(THREAD_ID, threadId); + + AgentBridge.getAgent().getLogSender().recordLogEvent(logEventMap); + } + } + } + } + + /** + * A LogEvent should be created if a log message or an error is logged. + * + * @param body Message to validate + * @param errorClass String to validate from OTel exception.type + * @param errorMessage String to validate from OTel exception.message + * @return true if a LogEvent should be created, otherwise false + */ + private static boolean shouldCreateLogEvent(Body body, String errorClass, String errorMessage) { + return (body != null) || (ExceptionUtil.getErrorClass(errorClass) != null) || (ExceptionUtil.getErrorMessage(errorMessage) != null); + } + + private static int calculateInitialMapSize(Attributes attributes) { + return isAppLoggingContextDataEnabled() && attributes != null + ? attributes.size() + DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES + : DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/span/AttributeKey.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/span/AttributeKey.java new file mode 100644 index 0000000000..1354c737a9 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/span/AttributeKey.java @@ -0,0 +1,30 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation.utils.span; + +import java.util.regex.Pattern; + +public class AttributeKey { + private static final Pattern SEMANTIC_CONVENTION_SPLIT = Pattern.compile(","); + + private final String key; + private final String [] semanticConventions; + + AttributeKey(String key, String semanticConventionString) { + this.key = key; + this.semanticConventions = SEMANTIC_CONVENTION_SPLIT.split(semanticConventionString); + } + + public String getKey() { + return key; + } + + + public String [] getSemanticConventions() { + return semanticConventions; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/span/AttributeMapper.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/span/AttributeMapper.java new file mode 100644 index 0000000000..1c9b9e4d1b --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/span/AttributeMapper.java @@ -0,0 +1,156 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation.utils.span; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.bootstrap.EmbeddedJarFilesImpl; +import io.opentelemetry.api.trace.SpanKind; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Level; + +public class AttributeMapper { + private static final String MAPPING_RESOURCE = "attribute-mappings.json"; + private static final String INSTRUMENTATION_JAR_LOCATION = "instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0-1.0"; + + private static volatile AttributeMapper instance; + private final Map>> mappings = new HashMap<>(); + + private AttributeMapper() { + for(SpanKind spanKind : SpanKind.values()) { + Map> typeToKeysMap = new HashMap<>(); + for (AttributeType attributeType : AttributeType.values()) { + typeToKeysMap.put(attributeType, new ArrayList<>()); + } + this.mappings.put(spanKind, typeToKeysMap); + } + } + + public static AttributeMapper getInstance() { + // Double-Checked Locking init + if (instance == null) { + synchronized (AttributeMapper.class) { + if (instance == null) { + try { + JSONArray rootArray = parseJsonResourceFromAgentJar(INSTRUMENTATION_JAR_LOCATION, MAPPING_RESOURCE); + + if (rootArray != null) { + instance = new AttributeMapper(); + + for (Object spanKindObj : rootArray) { + // Span kind and a list of attribute types + JSONObject spanKindObject = (JSONObject) spanKindObj; + SpanKind spanKind = SpanKind.valueOf((String) spanKindObject.get("spanKind")); + JSONArray jsonAttributeTypes = (JSONArray) spanKindObject.get("attributeTypes"); + + for (Object typeObj : jsonAttributeTypes) { + JSONObject categoryObject = (JSONObject) typeObj; + + // Grab the attribute type (Port, Host, etc) and then iterate over the actual attribute keys + AttributeType attributeType = AttributeType.valueOf((String) categoryObject.get("attributeType")); + JSONArray jsonAttributes = (JSONArray) categoryObject.get("attributes"); + for (Object jsonAttribute : jsonAttributes) { + JSONObject attribute = (JSONObject) jsonAttribute; + instance.addAttributeMapping(spanKind, attributeType, new AttributeKey((String) attribute.get("name"), (String) attribute.get("version"))); + } + } + } + } + } catch (Exception e) { + // Should never happen...Maybe + NewRelic.getAgent().getLogger().log(Level.SEVERE, "OTel AttributeMapper: Unable to read OTel attribute mappings: {0}", e.getMessage()); + } + } + } + } + + return instance; + } + + /** + * Based on the SpanKind and type of attribute, search through the available OTel keys and find the correct key + * since we don't know ahead of time what semantic convention version is in use and some keys vary based on + * that version. + * + * @param spanKind the span kind (SERVER, CLIENT, PRODUCER, CONSUMER, INTERNAL) + * @param type the "type" of key we're looking for: host, port, etc + * @param otelKeys the available OTel keys to search through + * + * @return the available key String + */ + public String findProperOtelKey(SpanKind spanKind, AttributeType type, Set otelKeys) { + List keys = mappings.get(spanKind).get(type); + for (AttributeKey key : keys) { + if (otelKeys.contains(key.getKey())) { + return key.getKey(); + } + } + + return ""; + } + + /** + * Visible for testing + * + * @return the configured mappings: SpanKind --> Map> + */ + Map>> getMappings() { + return mappings; + } + + /** + * Adds a new mapping to this mapper + * + * @param spanKind the SpanKind this mapping belongs to + * @param attributeType the type (Port, Host...) + * @param attributeKey the AttributeKey instance for this mapping + */ + private void addAttributeMapping(SpanKind spanKind, AttributeType attributeType, AttributeKey attributeKey) { + this.mappings.get(spanKind).get(attributeType).add(attributeKey); + } + + private static JSONArray parseJsonResourceFromAgentJar(String instrumentationJarLocation, String jsonResourceName) { + try (JarFile instrumentationJarFile = new JarFile(EmbeddedJarFilesImpl.INSTANCE.getJarFileInAgent(instrumentationJarLocation))) { + JarEntry jsonFileJarEntry = instrumentationJarFile.getJarEntry(jsonResourceName); + + if (jsonFileJarEntry != null) { + StringBuilder jsonStringBuilder = new StringBuilder(); + try (InputStream inputStream = instrumentationJarFile.getInputStream(jsonFileJarEntry)) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + jsonStringBuilder.append(new String(buffer, 0, bytesRead, StandardCharsets.UTF_8)); + } + } + String jsonString = jsonStringBuilder.toString(); + + JSONParser parser = new JSONParser(); + return (JSONArray) parser.parse(jsonString); + } else { + NewRelic.getAgent().getLogger().log(Level.SEVERE, "OTel AttributeMapper: JarEntry `{0}` not found in the {1} jar file", jsonResourceName, instrumentationJarLocation); + } + } catch (IOException e) { + NewRelic.getAgent().getLogger().log(Level.SEVERE, "OTel AttributeMapper: IOException constructing JarFile: {0}", e.getMessage()); + } catch (ParseException e) { + NewRelic.getAgent().getLogger().log(Level.SEVERE, "OTel AttributeMapper: ParseException parsing attribute mapping JSON: {0}", e.getMessage()); + } + return null; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/span/AttributeType.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/span/AttributeType.java new file mode 100644 index 0000000000..bc1d321aa0 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/com/nr/agent/instrumentation/utils/span/AttributeType.java @@ -0,0 +1,12 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation.utils.span; + +public enum AttributeType { + Port, Host, StatusCode, Method, Route, Component, Queue, RoutingKey, DBName, + DBOperation, DBSystem, DBStatement, DBTable, ExternalProcedure +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/context/ContextHelper.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/context/ContextHelper.java new file mode 100644 index 0000000000..b9fb3e87d7 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/context/ContextHelper.java @@ -0,0 +1,57 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.context; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.ExitTracer; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.api.agent.TracedMethod; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.sdk.trace.ExitTracerSpan; + +/** + * Helper class for managing the OpenTelemetry Context + */ +class ContextHelper { + private ContextHelper() { + } + + /** + * If there's no span on the context, but there is a NR tracer on the stack, return a context with our span. + */ + public static Context current(Context context) { + Span currentSpan = Span.fromContext(context); + if (currentSpan == Span.getInvalid()) { + Transaction transaction = AgentBridge.getAgent().getTransaction(false); + if (transaction != null) { + TracedMethod tracedMethod = transaction.getTracedMethod(); + if (tracedMethod instanceof ExitTracer) { + return context.with(ExitTracerSpan.wrap((ExitTracer) tracedMethod)); + } + } + } + return context; + } + + /** + * If there's currently no NR transaction but the current contains a NR span, create a + * {@link com.newrelic.api.agent.Token} related to that span's transaction and hook it into + * the returned {@link Scope}. + */ + public static Scope makeCurrent(Context context, Scope scope) { + final Transaction currentTransaction = AgentBridge.getAgent().getTransaction(false); + if (currentTransaction == null) { + Span currentSpan = Span.fromContext(context); + + if (currentSpan instanceof ExitTracerSpan) { + return ((ExitTracerSpan) currentSpan).createScope(scope); + } + } + return scope; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/context/Context_Instrumentation.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/context/Context_Instrumentation.java new file mode 100644 index 0000000000..7c6c83fa54 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/context/Context_Instrumentation.java @@ -0,0 +1,26 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.context; + +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +/** + * Weaved to manage the OpenTelemetry Context + */ +@Weave(type = MatchType.Interface, originalName = "io.opentelemetry.context.Context") +public abstract class Context_Instrumentation { + public static Context current() { + return ContextHelper.current(Weaver.callOriginal()); + } + + public Scope makeCurrent() { + return ContextHelper.makeCurrent((Context) this, Weaver.callOriginal()); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdk.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdk.java index a0fd8127ed..203faa59f4 100644 --- a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdk.java +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdk.java @@ -1,3 +1,10 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + package io.opentelemetry.sdk.autoconfigure; import com.newrelic.api.agent.NewRelic; @@ -7,24 +14,36 @@ import java.util.logging.Level; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.isOpenTelemetryMetricsEnabled; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.isOpenTelemetrySdkAutoConfigureEnabled; + +/** + * Weaved to autoconfigure the OpenTelemetrySDK properties + * and resources for compatability with New Relic. + */ @Weave(type = MatchType.ExactClass) public class AutoConfiguredOpenTelemetrySdk { /** * Creates a new {@link AutoConfiguredOpenTelemetrySdkBuilder} with the default configuration. - * If the agent configuration yaml, system property `-Dnewrelic.config.opentelemetry.sdk.autoconfigure.enabled`, - * or environment variable NEW_RELIC_OPENTELEMETRY_SDK_AUTOCONFIGURE_ENABLED is set to true, - * it will append customizers for properties and resources. + * If OTel Metrics signals are enabled, it will append customizers for properties and resources. * * @return a new {@link AutoConfiguredOpenTelemetrySdkBuilder} */ public static AutoConfiguredOpenTelemetrySdkBuilder builder() { final AutoConfiguredOpenTelemetrySdkBuilder builder = Weaver.callOriginal(); - Boolean autoConfigure = NewRelic.getAgent().getConfig().getValue("opentelemetry.sdk.autoconfigure.enabled", false); - if (autoConfigure == null || autoConfigure) { + + if (isOpenTelemetrySdkAutoConfigureEnabled() || isOpenTelemetryMetricsEnabled()) { + // Generate the instrumentation module enabled supportability metric + NewRelic.incrementCounter("Supportability/Metrics/Java/OpenTelemetryBridge/enabled"); + NewRelic.getAgent().getLogger().log(Level.INFO, "Appending OpenTelemetry SDK customizers"); - builder.addPropertiesCustomizer(new PropertiesCustomizer()); - builder.addResourceCustomizer(new ResourceCustomer()); + builder.addPropertiesCustomizer(OpenTelemetrySDKCustomizer::applyProperties); + builder.addResourceCustomizer(OpenTelemetrySDKCustomizer::applyResources); + builder.addMeterProviderCustomizer(OpenTelemetrySDKCustomizer::applyMeterExcludes); + } else { + // Generate the instrumentation module disabled supportability metric + NewRelic.incrementCounter("Supportability/Metrics/Java/OpenTelemetryBridge/disabled"); } return builder; } diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySDKCustomizer.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySDKCustomizer.java new file mode 100644 index 0000000000..a8f2ca7750 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySDKCustomizer.java @@ -0,0 +1,119 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.autoconfigure; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.Agent; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.NewRelic; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.ResourceBuilder; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; + +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.getOpenTelemetryMetricsExcludes; + +/** + * Helper class for customizing OpenTelemetrySDK properties + * and resources for compatability with New Relic. + */ +final class OpenTelemetrySDKCustomizer { + static final AttributeKey SERVICE_INSTANCE_ID_ATTRIBUTE_KEY = AttributeKey.stringKey("service.instance.id"); + + static Map applyProperties(ConfigProperties configProperties) { + return applyProperties(configProperties, NewRelic.getAgent()); + } + + /** + * Configure OpenTelemetry exporters to send data to the New Relic backend. + */ + static Map applyProperties(ConfigProperties configProperties, Agent agent) { + final String existingEndpoint = configProperties.getString("otel.exporter.otlp.endpoint"); + if (existingEndpoint == null) { + agent.getLogger().log(Level.INFO, "Auto-initializing OpenTelemetry SDK"); + final String host = agent.getConfig().getValue("host"); + final String endpoint = "https://" + host + ":443"; + final String licenseKey = agent.getConfig().getValue("license_key"); + final Map properties = new HashMap<>(); + properties.put("otel.exporter.otlp.headers", "api-key=" + licenseKey); + properties.put("otel.exporter.otlp.endpoint", endpoint); + properties.put("otel.metrics.exporter", "otlp"); // enable otlp metrics exporter + properties.put("otel.traces.exporter", "none"); // disable default traces exporter + properties.put("otel.logs.exporter", "none"); // disable default logs exporter + properties.put("otel.exporter.otlp.protocol", "http/protobuf"); + properties.put("otel.span.attribute.value.length.limit", "4095"); + properties.put("otel.exporter.otlp.compression", "gzip"); + properties.put("otel.exporter.otlp.metrics.temporality.preference", "DELTA"); + properties.put("otel.exporter.otlp.metrics.default.histogram.aggregation", "BASE2_EXPONENTIAL_BUCKET_HISTOGRAM"); + properties.put("otel.experimental.exporter.otlp.retry.enabled", "true"); + properties.put("otel.experimental.resource.disabled.keys", "process.command_line"); + + final Object appName = agent.getConfig().getValue("app_name"); + properties.put("otel.service.name", appName.toString()); + + return properties; + } else { + agent.getLogger().log(Level.WARNING, + "The OpenTelemetry exporter endpoint is set to {0}, the agent will not autoconfigure the SDK", + existingEndpoint); + } + return Collections.emptyMap(); + } + + static Resource applyResources(Resource resource, ConfigProperties configProperties) { + return applyResources(resource, AgentBridge.getAgent(), NewRelic.getAgent().getLogger()); + } + + /** + * Add the monitored service's entity.guid to resources. + */ + static Resource applyResources(Resource resource, com.newrelic.agent.bridge.Agent agent, Logger logger) { + logger.log(Level.FINE, "Appending OpenTelemetry resources"); + final ResourceBuilder builder = new ResourceBuilder().putAll(resource); + final String instanceId = resource.getAttribute(SERVICE_INSTANCE_ID_ATTRIBUTE_KEY); + if (instanceId == null) { + builder.put(SERVICE_INSTANCE_ID_ATTRIBUTE_KEY, UUID.randomUUID().toString()); + } + + final String entityGuid = agent.getEntityGuid(true); + if (entityGuid != null) { + builder.put("entity.guid", entityGuid); + } + return builder.build(); + } + + /** + * Read list of excluded meters, and customize the meter provider to drop any with matching names. + */ + static SdkMeterProviderBuilder applyMeterExcludes(SdkMeterProviderBuilder sdkMeterProviderBuilder, ConfigProperties configProperties) { + return applyMeterExcludes(sdkMeterProviderBuilder, NewRelic.getAgent()); + } + + static SdkMeterProviderBuilder applyMeterExcludes(SdkMeterProviderBuilder sdkMeterProviderBuilder, Agent agent) { + final List excludedMeters = getOpenTelemetryMetricsExcludes(); + agent.getLogger().log(Level.FINE, "Suppressing excluded OpenTelemetry meters: {0}", excludedMeters); + for (String meterName : excludedMeters) { + sdkMeterProviderBuilder.registerView( + InstrumentSelector.builder().setMeterName(meterName).build(), + View.builder().setAggregation(Aggregation.drop()).build() + ); + } + return sdkMeterProviderBuilder; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/PropertiesCustomizer.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/PropertiesCustomizer.java deleted file mode 100644 index cc8496c246..0000000000 --- a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/PropertiesCustomizer.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.opentelemetry.sdk.autoconfigure; - -import com.newrelic.api.agent.Agent; -import com.newrelic.api.agent.NewRelic; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; -import java.util.logging.Level; - -public final class PropertiesCustomizer implements Function> { - @Override - public Map apply(ConfigProperties configProperties) { - if (configProperties.getString("otel.exporter.otlp.endpoint") == null) { - final Agent agent = NewRelic.getAgent(); - agent.getLogger().log(Level.INFO, "Auto-initializing OpenTelemetry SDK"); - final String host = agent.getConfig().getValue("host"); - final String endpoint = "https://" + host + ":443"; - final String licenseKey = agent.getConfig().getValue("license_key"); - final Map properties = new HashMap<>(); - properties.put("otel.exporter.otlp.headers", "api-key=" + licenseKey); - properties.put("otel.exporter.otlp.endpoint", endpoint); - properties.put("otel.exporter.otlp.protocol", "http/protobuf"); - properties.put("otel.span.attribute.value.length.limit", "4095"); - properties.put("otel.exporter.otlp.compression", "gzip"); - properties.put("otel.exporter.otlp.metrics.temporality.preference", "DELTA"); - properties.put("otel.exporter.otlp.metrics.default.histogram.aggregation", "BASE2_EXPONENTIAL_BUCKET_HISTOGRAM"); - properties.put("otel.experimental.exporter.otlp.retry.enabled", "true"); - properties.put("otel.experimental.resource.disabled.keys", "process.command_line"); - - final Object appName = agent.getConfig().getValue("app_name"); - properties.put("otel.service.name", appName.toString()); - - return properties; - } - return Collections.emptyMap(); - } -} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceCustomer.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceCustomer.java deleted file mode 100644 index 33868c9071..0000000000 --- a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/ResourceCustomer.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.opentelemetry.sdk.autoconfigure; - -import com.newrelic.agent.bridge.AgentBridge; -import com.newrelic.api.agent.NewRelic; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.resources.ResourceBuilder; - -import java.util.UUID; -import java.util.function.BiFunction; -import java.util.logging.Level; - -public final class ResourceCustomer implements BiFunction { - @Override - public Resource apply(Resource resource, ConfigProperties configProperties) { - NewRelic.getAgent().getLogger().log(Level.FINE, "Appending OpenTelemetry resources"); - final ResourceBuilder builder = new ResourceBuilder().putAll(resource); - final AttributeKey instanceIdKey = AttributeKey.stringKey("service.instance.id"); - final String instanceId = resource.getAttribute(instanceIdKey); - if (instanceId == null) { - builder.put(instanceIdKey, UUID.randomUUID().toString()); - } - - final String entityGuid = AgentBridge.getAgent().getEntityGuid(true); - if (entityGuid != null) { - builder.put("entity.guid", entityGuid); - } - return builder.build(); - } -} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLogRecord.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLogRecord.java new file mode 100644 index 0000000000..0121039011 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLogRecord.java @@ -0,0 +1,300 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import com.nr.agent.instrumentation.utils.AttributesHelper; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.Body; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; + +import java.util.Map; + +/** + * New Relic Java representation of an OpenTelemetry LogRecord. + */ +public class NRLogRecord implements ReadWriteLogRecord { + public static final AttributeKey OTEL_SCOPE_VERSION = AttributeKey.stringKey("otel.scope.version"); + public static final AttributeKey OTEL_SCOPE_NAME = AttributeKey.stringKey("otel.scope.name"); + public static final AttributeKey OTEL_LIBRARY_VERSION = AttributeKey.stringKey("otel.library.version"); + public static final AttributeKey OTEL_LIBRARY_NAME = AttributeKey.stringKey("otel.library.name"); + public static final AttributeKey OTEL_EXCEPTION_MESSAGE = AttributeKey.stringKey("exception.message"); + public static final AttributeKey OTEL_EXCEPTION_TYPE = AttributeKey.stringKey("exception.type"); + public static final AttributeKey OTEL_EXCEPTION_STACKTRACE = AttributeKey.stringKey("exception.stacktrace"); + public static final AttributeKey THREAD_NAME = AttributeKey.stringKey("thread.name"); + public static final AttributeKey THREAD_ID_LONG = AttributeKey.longKey("thread.id"); + public static final AttributeKey THREAD_ID_STRING = AttributeKey.stringKey("thread.id"); + + private final LogLimits logLimits; // LogLimits is not used in this implementation, but kept for compatibility + private final Resource resource; + private final InstrumentationScopeInfo instrumentationScopeInfo; + private final long timestampEpochNanos; + private final long observedTimestampEpochNanos; + private final SpanContext spanContext; + private final Severity severity; + private final String severityText; + private final Body body; + private final Object lock = new Object(); + private final Map attributes; + + private NRLogRecord( + LogLimits logLimits, + Resource resource, + InstrumentationScopeInfo instrumentationScopeInfo, + long timestampEpochNanos, + long observedTimestampEpochNanos, + SpanContext spanContext, + Severity severity, + String severityText, + Body body, + Map attributes + ) { + this.logLimits = logLimits; + this.resource = resource; + this.instrumentationScopeInfo = instrumentationScopeInfo; + this.timestampEpochNanos = timestampEpochNanos; + this.observedTimestampEpochNanos = observedTimestampEpochNanos; + this.spanContext = spanContext; + this.severity = severity; + this.severityText = severityText; + this.body = body; + this.attributes = attributes; + } + + /** + * Create the OTel LogRecord. + */ + static NRLogRecord create( + LogLimits logLimits, + Resource resource, + InstrumentationScopeInfo instrumentationScopeInfo, + long timestampEpochNanos, + long observedTimestampEpochNanos, + SpanContext spanContext, + Severity severity, + String severityText, + Body body, + Map attributes) { + return new NRLogRecord( + logLimits, + resource, + instrumentationScopeInfo, + timestampEpochNanos, + observedTimestampEpochNanos, + spanContext, + severity, + severityText, + body, + attributes); + } + + @Override + public ReadWriteLogRecord setAttribute(AttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + synchronized (lock) { + attributes.put(key.getKey(), value); + } + return this; + } + + /** + * @return an immutable LogRecordData instance representing this log record. + */ + @Override + public LogRecordData toLogRecordData() { + synchronized (lock) { + return BasicLogRecordData.create( + resource, + instrumentationScopeInfo, + timestampEpochNanos, + observedTimestampEpochNanos, + spanContext, + severity, + severityText, + body, + AttributesHelper.toAttributes(attributes), + attributes.size() + ); + } + } + + public static class BasicLogRecordData implements LogRecordData { + private final Resource resource; + private final InstrumentationScopeInfo instrumentationScopeInfo; + private final long timestampEpochNanos; + private final long observedTimestampEpochNanos; + private final SpanContext spanContext; + private final Severity severity; + private final String severityText; + private final Body body; + private final Attributes attributes; + private final int totalAttributeCount; + + private final long threadId; + private final String threadName; + + private BasicLogRecordData( + Resource resource, + InstrumentationScopeInfo instrumentationScopeInfo, + long timestampEpochNanos, + long observedTimestampEpochNanos, + SpanContext spanContext, + Severity severity, + String severityText, + Body body, + Attributes attributes, + int totalAttributeCount) { + this.resource = resource; + this.instrumentationScopeInfo = instrumentationScopeInfo; + this.timestampEpochNanos = timestampEpochNanos; + this.observedTimestampEpochNanos = observedTimestampEpochNanos; + this.spanContext = spanContext; + this.severity = severity; + this.severityText = severityText; + this.body = body; + this.attributes = attributes; + this.totalAttributeCount = totalAttributeCount; + // The thread.name and thread.id attributes are considered to be experimental and + // are not guaranteed to be present in the attributes. If they are present, they will + // be used. If they are not present, we generate the thread information ourselves. + String threadName = attributes.get(THREAD_NAME); + if (threadName != null && !threadName.isEmpty()) { + this.threadName = threadName; + } else { + this.threadName = Thread.currentThread().getName(); + } + Long threadIdLong = attributes.get(THREAD_ID_LONG); + String threadIdString = attributes.get(THREAD_ID_STRING); + if (threadIdLong != null && threadIdLong > -1) { + this.threadId = threadIdLong; + } else if (threadIdString != null && !threadIdString.isEmpty()) { + this.threadId = Long.parseLong(threadIdString); + } else { + this.threadId = Thread.currentThread().getId(); + } + } + + static BasicLogRecordData create( + Resource resource, + InstrumentationScopeInfo instrumentationScopeInfo, + long timestampEpochNanos, + long observedTimestampEpochNanos, + SpanContext spanContext, + Severity severity, + String severityText, + Body body, + Attributes attributes, + int totalAttributeCount) { + return new BasicLogRecordData( + resource, + instrumentationScopeInfo, + timestampEpochNanos, + observedTimestampEpochNanos, + spanContext, + severity, + severityText, + body, + attributes, + totalAttributeCount + ); + } + + public long getThreadId() { + return threadId; + } + + public String getThreadName() { + return threadName; + } + + @Override + public Resource getResource() { + return resource; + } + + @Override + public InstrumentationScopeInfo getInstrumentationScopeInfo() { + return instrumentationScopeInfo; + } + + @Override + public long getTimestampEpochNanos() { + return timestampEpochNanos; + } + + @Override + public long getObservedTimestampEpochNanos() { + return observedTimestampEpochNanos; + } + + @Override + public SpanContext getSpanContext() { + return spanContext; + } + + @Override + public Severity getSeverity() { + return severity; + } + + @Override + public String getSeverityText() { + return severityText; + } + + @Override + public Body getBody() { + return body; + } + + @Override + public Attributes getAttributes() { + return attributes; + } + + @Override + public int getTotalAttributeCount() { + return totalAttributeCount; + } + + @Override + public String toString() { + return "BasicLogRecordData{resource=" + + this.resource + + ", instrumentationScopeInfo=" + + this.instrumentationScopeInfo + + ", timestampEpochNanos=" + + this.timestampEpochNanos + + ", observedTimestampEpochNanos=" + + this.observedTimestampEpochNanos + + ", spanContext=" + + this.spanContext + + ", severity=" + + this.severity + + ", severityText=" + + this.severityText + + ", body=" + + this.body + + ", attributes=" + + this.attributes + + ", totalAttributeCount=" + + this.totalAttributeCount + + ", threadId=" + + this.threadId + + ", threadName=" + + this.threadName + + "}"; + } + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLogRecordBuilder.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLogRecordBuilder.java new file mode 100644 index 0000000000..f92f5abdbe --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLogRecordBuilder.java @@ -0,0 +1,161 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import com.newrelic.agent.bridge.logging.AppLoggingUtils; +import com.newrelic.api.agent.NewRelic; +import com.nr.agent.instrumentation.utils.logs.LogEventUtil; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.logs.data.Body; +import io.opentelemetry.sdk.logs.data.LogRecordData; + +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * New Relic Java agent implementation of an OpenTelemetry LogRecordBuilder, + * which is used to build and emit OTel LogRecord instances. In addition to + * emitting an OpenTelemetry LogRecord, this implementation will create a + * New Relic LogEvent. + */ +public class NRLogRecordBuilder implements LogRecordBuilder { + private final Map attributes = new HashMap<>(); + private final LoggerSharedState loggerSharedState; + private final InstrumentationScopeInfo instrumentationScopeInfo; + + private long timestampEpochNanos; + private long observedTimestampEpochNanos; + private Context context; + private Severity severity = Severity.UNDEFINED_SEVERITY_NUMBER; + private String severityText; + private Body body = Body.empty(); + + public NRLogRecordBuilder(String instrumentationScopeName, String instrumentationScopeVersion, String schemaUrl, LoggerSharedState loggerSharedState) { + this.loggerSharedState = loggerSharedState; + this.instrumentationScopeInfo = InstrumentationScopeInfo + .builder(instrumentationScopeName) + .setVersion(instrumentationScopeVersion) + .setSchemaUrl(schemaUrl) + .build(); + } + + @Override + public LogRecordBuilder setTimestamp(long timestamp, TimeUnit unit) { + this.timestampEpochNanos = unit.toNanos(timestamp); + return this; + } + + @Override + public LogRecordBuilder setTimestamp(Instant instant) { + this.timestampEpochNanos = TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano(); + return this; + } + + @Override + public LogRecordBuilder setObservedTimestamp(long timestamp, TimeUnit unit) { + this.observedTimestampEpochNanos = unit.toNanos(timestamp); + return this; + } + + @Override + public LogRecordBuilder setObservedTimestamp(Instant instant) { + this.observedTimestampEpochNanos = TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano(); + return this; + } + + @Override + public LogRecordBuilder setContext(Context context) { + this.context = context; + return this; + } + + @Override + public LogRecordBuilder setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + @Override + public LogRecordBuilder setSeverityText(String severityText) { + this.severityText = severityText; + return this; + } + + @Override + public LogRecordBuilder setBody(String body) { + this.body = Body.string(body); + return this; + } + + @Override + public LogRecordBuilder setAttribute(AttributeKey key, T value) { + if (key == null || key.getKey().isEmpty() || value == null) { + return this; + } + this.attributes.put(key.getKey(), value); + return this; + } + + /** + * Intercept here to create a NR LogEvent from the OpenTelemetry + * LogRecord that is being emitted. The OpenTelemetry LogRecord + * will still be emitted to its configured destination. + */ + @Override + public void emit() { + if (loggerSharedState.hasBeenShutdown()) { + return; + } + Context context = this.context == null ? Context.current() : this.context; + + long observedTimestampEpochNanos = + this.observedTimestampEpochNanos == 0 + ? this.loggerSharedState.getClock().now() + : this.observedTimestampEpochNanos; + + NRLogRecord nrLogRecord = NRLogRecord.create( + loggerSharedState.getLogLimits(), + loggerSharedState.getResource(), + instrumentationScopeInfo, + timestampEpochNanos, + observedTimestampEpochNanos, + Span.fromContext(context).getSpanContext(), + severity, + severityText, + body, + Collections.unmodifiableMap(new HashMap<>(attributes)) + ); + + // Pass NRLogRecord through to user configured logRecordProcessor + // so that the LogRecords get written to the expected destination. + loggerSharedState.getLogRecordProcessor().onEmit(context, nrLogRecord); + + LogRecordData logRecordData = nrLogRecord.toLogRecordData(); + + if (AppLoggingUtils.isApplicationLoggingEnabled()) { + if (AppLoggingUtils.isApplicationLoggingMetricsEnabled()) { + // Generate log level metrics + NewRelic.incrementCounter("Logging/lines"); + NewRelic.incrementCounter("Logging/lines/" + severity.name()); + } + + if (AppLoggingUtils.isApplicationLoggingForwardingEnabled()) { + // Generate New Relic LogEvents + LogEventUtil.recordNewRelicLogEvent(logRecordData); + } + } + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLoggerBuilder.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLoggerBuilder.java new file mode 100644 index 0000000000..961dbc6287 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLoggerBuilder.java @@ -0,0 +1,45 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerBuilder; + +/** + * New Relic Java agent implementation of an OpenTelemetry + * LoggerBuilder, which is a factory for building OpenTelemetry Loggers. + * An OpenTelemetry Logger can then be used to build and emit OpenTelemetry LogRecords. + */ +class NRLoggerBuilder implements LoggerBuilder { + private final String instrumentationScopeName; + private final LoggerSharedState sharedState; + private String schemaUrl; + private String instrumentationScopeVersion; + + public NRLoggerBuilder(String instrumentationScopeName, LoggerSharedState sharedState) { + this.instrumentationScopeName = instrumentationScopeName; + this.sharedState = sharedState; + } + + @Override + public LoggerBuilder setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + @Override + public LoggerBuilder setInstrumentationVersion(String instrumentationScopeVersion) { + this.instrumentationScopeVersion = instrumentationScopeVersion; + return this; + } + + @Override + public Logger build() { + return () -> new NRLogRecordBuilder(instrumentationScopeName, instrumentationScopeVersion, schemaUrl, sharedState); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider_Instrumentation.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider_Instrumentation.java new file mode 100644 index 0000000000..eaf0393ff4 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider_Instrumentation.java @@ -0,0 +1,48 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import io.opentelemetry.api.logs.LoggerBuilder; + +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.isOpenTelemetryLogsEnabled; + +/** + * Weaved to inject a New Relic Java agent implementation of an OpenTelemetry LoggerBuilder + */ +@Weave(type = MatchType.ExactClass, originalName = "io.opentelemetry.sdk.logs.SdkLoggerProvider") +public final class SdkLoggerProvider_Instrumentation { + private final LoggerSharedState sharedState = Weaver.callOriginal(); + + public LoggerBuilder loggerBuilder(String instrumentationScopeName) { + final LoggerBuilder loggerBuilder = Weaver.callOriginal(); + if (isOpenTelemetryLogsEnabled()) { + // Generate the instrumentation module enabled supportability metric + NewRelic.incrementCounter("Supportability/Logging/Java/OpenTelemetryBridge/enabled"); + // return our logger builder instead of the OTel instance + return new NRLoggerBuilder(instrumentationNameOrDefault(instrumentationScopeName), sharedState); + } else { + // Generate the instrumentation module disabled supportability metric + NewRelic.incrementCounter("Supportability/Logging/Java/OpenTelemetryBridge/disabled"); + } + return loggerBuilder; + } + + /** + * Returns the instrumentation name or the default value of "unknown" if not set. + * + * @param instrumentationScopeName the name of the instrumentation scope + * @return the instrumentation name or a default value + */ + private static String instrumentationNameOrDefault(String instrumentationScopeName) { + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java new file mode 100644 index 0000000000..4fe79f19e1 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java @@ -0,0 +1,430 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.ExitTracer; +import com.newrelic.agent.bridge.datastore.SqlQueryConverter; +import com.newrelic.agent.tracers.TracerFlags; +import com.newrelic.api.agent.DatastoreParameters; +import com.newrelic.api.agent.HttpParameters; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Token; +import com.nr.agent.instrumentation.utils.AttributesHelper; +import com.nr.agent.instrumentation.utils.span.AttributeMapper; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributeType; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Representation of a Span + */ +public class ExitTracerSpan implements ReadWriteSpan { + // otel.scope.version and otel.scope.name should be reported along with the deprecated versions otel.library.version and otel.library.name + static final String OTEL_SCOPE_VERSION = "otel.scope.version"; + static final AttributeKey OTEL_SCOPE_NAME = AttributeKey.stringKey("otel.scope.name"); + static final String OTEL_LIBRARY_VERSION = "otel.library.version"; + static final AttributeKey OTEL_LIBRARY_NAME = AttributeKey.stringKey("otel.library.name"); + + private static final AttributeKey DB_SYSTEM = AttributeKey.stringKey("db.system"); + private static final AttributeKey DB_STATEMENT = AttributeKey.stringKey("db.statement"); + private static final AttributeKey DB_OPERATION = AttributeKey.stringKey("db.operation"); + private static final AttributeKey DB_SQL_TABLE = AttributeKey.stringKey("db.sql.table"); + + private static final AttributeKey SERVER_ADDRESS = AttributeKey.stringKey("server.address"); + private static final AttributeKey SERVER_PORT = AttributeKey.longKey("server.port"); + + // these attributes are reported as agent attributes, we don't want to duplicate them in user attributes + private static final Set AGENT_ATTRIBUTE_KEYS = + Collections.unmodifiableSet( + Stream.of(DB_STATEMENT, DB_SQL_TABLE, DB_SYSTEM, DB_OPERATION, SERVER_ADDRESS, SERVER_PORT) + .map(AttributeKey::getKey) + .collect(Collectors.toSet())); + + final ExitTracer tracer; + private final SpanKind spanKind; + private final InstrumentationLibraryInfo instrumentationLibraryInfo; + private final Map attributes; + private final SpanContext spanContext; + private final Consumer onEnd; + private final SpanContext parentSpanContext; + private final long startEpochNanos; + private boolean ended; + private String spanName; + private long endEpochNanos; + private final Resource resource; + private final AttributeMapper attributeMapper = AttributeMapper.getInstance(); + + ExitTracerSpan(ExitTracer tracer, InstrumentationLibraryInfo instrumentationLibraryInfo, SpanKind spanKind, String spanName, SpanContext parentSpanContext, + Resource resource, Map attributes, Consumer onEnd) { + this.tracer = tracer; + this.spanKind = spanKind; + this.spanName = spanName; + this.parentSpanContext = parentSpanContext; + this.attributes = attributes; + this.onEnd = onEnd; + this.resource = resource; + this.instrumentationLibraryInfo = instrumentationLibraryInfo; + this.startEpochNanos = System.nanoTime(); + this.spanContext = SpanContext.create(tracer.getTraceId(), tracer.getSpanId(), TraceFlags.getDefault(), TraceState.getDefault()); + this.setAllAttributes(resource.getAttributes()); + } + + public static ExitTracerSpan wrap(ExitTracer tracer) { + return new ExitTracerSpan(tracer, InstrumentationLibraryInfo.empty(), SpanKind.INTERNAL, tracer.getMetricName(), SpanContext.getInvalid(), + Resource.empty(), Collections.emptyMap(), span -> { + }); + } + + @Override + public Span setAttribute(AttributeKey key, T value) { + attributes.put(key.getKey(), value); + return this; + } + + @Override + public Span addEvent(String name, Attributes attributes) { + return this; + } + + @Override + public Span addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { + return this; + } + + @Override + public Span setStatus(StatusCode statusCode, String description) { + return this; + } + + @Override + public Span recordException(Throwable exception) { + NewRelic.noticeError(exception); + return this; + } + + @Override + public Span recordException(Throwable exception, Attributes additionalAttributes) { + NewRelic.noticeError(exception, toMap(additionalAttributes)); + return this; + } + + static Map toMap(Attributes attributes) { + final Map map = new HashMap<>(attributes.size()); + attributes.forEach((key, value) -> { + switch (key.getType()) { + case STRING: + case LONG: + case DOUBLE: + case BOOLEAN: + map.put(key.getKey(), value); + break; + } + }); + return map; + } + + @Override + public Span updateName(String name) { + this.spanName = name; + return this; + } + + @Override + public void end() { + if (SpanKind.CLIENT == spanKind) { + reportClientSpan(); + } + tracer.setMetricName("Span", spanName); + // db.statement is reported through DatastoreParameters.SlowQueryParameter. That code path + // will correctly obfuscate the sql based on agent settings. + Map filteredAttributes = attributes.entrySet().stream() + .filter(entry -> !AGENT_ATTRIBUTE_KEYS.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + tracer.addCustomAttributes(filteredAttributes); + tracer.finish(); + endEpochNanos = System.nanoTime(); + ended = true; + onEnd.accept(this); + } + + @Override + public void end(long timestamp, TimeUnit unit) { + this.end(); + } + + @Override + public SpanContext getSpanContext() { + return spanContext; + } + + @Override + public boolean isRecording() { + return true; + } + + @Override + public SpanContext getParentSpanContext() { + return parentSpanContext; + } + + @Override + public String getName() { + return spanName; + } + + @Override + public SpanData toSpanData() { + return new BasicSpanData(spanName, endEpochNanos, AttributesHelper.toAttributes(attributes), ended); + } + + @Override + public InstrumentationLibraryInfo getInstrumentationLibraryInfo() { + return InstrumentationLibraryInfo.empty(); + } + + @Override + public boolean hasEnded() { + return ended; + } + + @Override + public long getLatencyNanos() { + long endEpochNanos = ended ? this.endEpochNanos : System.nanoTime(); + return endEpochNanos - startEpochNanos; + } + + @Override + public SpanKind getKind() { + return spanKind; + } + + public T getAttribute(AttributeKey key) { + Object value = attributes.get(key.getKey()); + if (key.getType() == AttributeType.LONG && value instanceof Number) { + value = ((Number) value).longValue(); + } else if (key.getType() == AttributeType.DOUBLE && value instanceof Number) { + value = ((Number) value).doubleValue(); + } + return (T) value; + } + + private void reportClientSpan() { + final String dbSystem = getAttribute(generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.DBSystem)); + if (dbSystem != null) { + String operation = getAttribute(generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.DBOperation)); + DatastoreParameters.InstanceParameter builder = DatastoreParameters + .product(dbSystem) + .collection(getAttribute(generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.DBTable))) + .operation(operation == null ? "unknown" : operation); + String serverAddress = getAttribute(generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.Host)); + Long serverPort = getAttribute(generateLongAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.Port)); + + DatastoreParameters.DatabaseParameter instance = serverAddress == null ? builder.noInstance() : + builder.instance(serverAddress, (serverPort == null ? Long.valueOf(0L) : serverPort).intValue()); + + String dbName = getAttribute(generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.DBName)); + DatastoreParameters.SlowQueryParameter slowQueryParameter = + dbName == null ? instance.noDatabaseName() : instance.databaseName(dbName); + final String dbStatement = getAttribute(generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.DBStatement)); + final DatastoreParameters datastoreParameters; + if (dbStatement == null) { + datastoreParameters = slowQueryParameter.build(); + } else { + datastoreParameters = slowQueryParameter.slowQuery(dbStatement, SqlQueryConverter.INSTANCE).build(); + } + + tracer.reportAsExternal(datastoreParameters); + } + // Only support the current otel spec. Ignore client spans with old attribute names + else { + try { + final URI uri = getUri(); + if (uri != null) { + final String libraryName = getAttribute(OTEL_LIBRARY_NAME); + HttpParameters genericParameters = HttpParameters.library(libraryName).uri(uri) + .procedure(getProcedure()).noInboundHeaders().build(); + tracer.reportAsExternal(genericParameters); + } + } catch (URISyntaxException e) { + NewRelic.getAgent().getLogger().log(Level.FINER, "Error parsing client span uri", e); + } + } + } + + private AttributeKey generateStringAttributeKey(SpanKind spanKind, com.nr.agent.instrumentation.utils.span.AttributeType attributeType) { + return AttributeKey.stringKey( + attributeMapper.findProperOtelKey(spanKind, attributeType, attributes.keySet())); + } + + private AttributeKey generateLongAttributeKey(SpanKind spanKind, com.nr.agent.instrumentation.utils.span.AttributeType attributeType) { + return AttributeKey.longKey( + attributeMapper.findProperOtelKey(spanKind, attributeType, attributes.keySet())); + } + + + String getProcedure() { + AttributeKey key = generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.ExternalProcedure); + if (key.getKey().isEmpty()) { + key = generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.Method); + } + return key.getKey().isEmpty() ? "unknown" : getAttribute(key); + } + + URI getUri() throws URISyntaxException { + final String urlFull = getAttribute(generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.Route)); + if (urlFull != null) { + return URI.create(urlFull); + } else { + final String serverAddress = getAttribute(generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.Host)); + if (serverAddress != null) { + final String scheme = getAttribute(generateStringAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.Route)); + final Long serverPort = getAttribute(generateLongAttributeKey(SpanKind.CLIENT, com.nr.agent.instrumentation.utils.span.AttributeType.Port)); + return new URI(scheme == null ? "http" : scheme, null, serverAddress, + serverPort == null ? 0 : serverPort.intValue(), null, null, null); + } + } + return null; + } + + public Scope createScope(Scope scope) { + final Token token = tracer.getToken(); + // we can't link a known transaction from one thread to another unless there is + // a transaction with at least one tracer on the new thread. + AgentBridge.getAgent().getTransaction(true); + final ExitTracer tracer = AgentBridge.instrumentation.createTracer(null, + TracerFlags.CUSTOM | TracerFlags.ASYNC); + tracer.setMetricName("Java", "OpenTelemetry", "AsyncScope"); + token.link(); + return () -> { + token.expire(); + tracer.finish(); + scope.close(); + }; + } + + public class BasicSpanData implements SpanData { + private final String spanName; + private final long endEpochNanos; + private final Attributes attributes; + private final boolean ended; + + public BasicSpanData(String spanName, long endEpochNanos, Attributes attributes, boolean ended) { + this.spanName = spanName; + this.endEpochNanos = endEpochNanos; + this.attributes = attributes; + this.ended = ended; + } + + @Override + public String getName() { + return spanName; + } + + @Override + public SpanKind getKind() { + return spanKind; + } + + @Override + public SpanContext getSpanContext() { + return spanContext; + } + + @Override + public SpanContext getParentSpanContext() { + return parentSpanContext; + } + + @Override + public StatusData getStatus() { + return StatusData.ok(); + } + + @Override + public long getStartEpochNanos() { + return startEpochNanos; + } + + @Override + public Attributes getAttributes() { + return attributes; + } + + @Override + public List getEvents() { + return Collections.emptyList(); + } + + @Override + public List getLinks() { + return Collections.emptyList(); + } + + @Override + public long getEndEpochNanos() { + return endEpochNanos; + } + + @Override + public boolean hasEnded() { + return ended; + } + + @Override + public int getTotalRecordedEvents() { + return 0; + } + + @Override + public int getTotalRecordedLinks() { + return 0; + } + + @Override + public int getTotalAttributeCount() { + return 0; + } + + @Override + public InstrumentationLibraryInfo getInstrumentationLibraryInfo() { + return instrumentationLibraryInfo; + } + + @Override + public Resource getResource() { + return resource; + } + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java new file mode 100644 index 0000000000..ddb172f0d2 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java @@ -0,0 +1,322 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.ExitTracer; +import com.newrelic.agent.bridge.Instrumentation; +import com.newrelic.agent.bridge.Transaction; +import com.newrelic.agent.tracers.TracerFlags; +import com.newrelic.api.agent.ExtendedRequest; +import com.newrelic.api.agent.ExtendedResponse; +import com.newrelic.api.agent.HeaderType; +import com.newrelic.api.agent.TracedMethod; +import com.nr.agent.instrumentation.utils.header.W3CTraceParentHeader; +import com.nr.agent.instrumentation.utils.span.AttributeMapper; +import com.nr.agent.instrumentation.utils.span.AttributeType; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static com.nr.agent.instrumentation.utils.header.HeaderType.NEWRELIC; +import static com.nr.agent.instrumentation.utils.header.HeaderType.W3C_TRACEPARENT; +import static com.nr.agent.instrumentation.utils.header.HeaderType.W3C_TRACESTATE; + +/** + * New Relic Java agent implementation of an OpenTelemetry SpanBuilder, + * which is used to construct Span instances. Instead of starting an OpenTelemetry + * Span, this implementation will create a New Relic Java agent Tracer to time + * the executing code and will potentially start a New Relic Java agent Transaction + * based on the detected SpanKind type. + */ +class NRSpanBuilder implements SpanBuilder { + private static final Span NO_OP_SPAN = OpenTelemetry.noop().getTracer("").spanBuilder("").startSpan(); + private final Instrumentation instrumentation; + private final String spanName; + private final Map attributes = new HashMap<>(); + private final TracerSharedState sharedState; + private final Consumer endHandler; + private final InstrumentationLibraryInfo instrumentationLibraryInfo; + private SpanKind spanKind = SpanKind.INTERNAL; + private SpanContext parentSpanContext; + private AttributeMapper attributeMapper = AttributeMapper.getInstance(); + + public NRSpanBuilder(Instrumentation instrumentation, String instrumentationScopeName, String instrumentationScopeVersion, TracerSharedState sharedState, + String spanName) { + this.instrumentation = instrumentation; + this.spanName = spanName; + this.sharedState = sharedState; + instrumentationLibraryInfo = InstrumentationLibraryInfo.create(instrumentationScopeName, instrumentationScopeVersion); + attributes.put(ExitTracerSpan.OTEL_SCOPE_NAME.getKey(), instrumentationScopeName); + attributes.put(ExitTracerSpan.OTEL_LIBRARY_NAME.getKey(), instrumentationScopeName); + if (instrumentationScopeVersion != null) { + attributes.put(ExitTracerSpan.OTEL_SCOPE_VERSION, instrumentationScopeVersion); + attributes.put(ExitTracerSpan.OTEL_LIBRARY_VERSION, instrumentationScopeVersion); + } + if (sharedState.getActiveSpanProcessor().isEndRequired()) { + endHandler = sharedState.getActiveSpanProcessor()::onEnd; + } else { + endHandler = span -> { + }; + } + } + + @Override + public SpanBuilder setParent(Context context) { + parentSpanContext = Span.fromContext(context).getSpanContext(); + return this; + } + + @Override + public SpanBuilder setNoParent() { + return this; + } + + @Override + public SpanBuilder addLink(SpanContext spanContext) { + return this; + } + + @Override + public SpanBuilder addLink(SpanContext spanContext, Attributes attributes) { + return this; + } + + @Override + public SpanBuilder setAttribute(String key, String value) { + attributes.put(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, long value) { + attributes.put(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, double value) { + attributes.put(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(String key, boolean value) { + attributes.put(key, value); + return this; + } + + @Override + public SpanBuilder setAttribute(AttributeKey key, T value) { + attributes.put(key.getKey(), value); + return this; + } + + @Override + public SpanBuilder setSpanKind(SpanKind spanKind) { + this.spanKind = spanKind; + return this; + } + + @Override + public SpanBuilder setStartTimestamp(long startTimestamp, TimeUnit unit) { + return this; + } + + /** + * Called when starting an OpenTelemetry Span and will result in a New Relic + * Java agent Tracer being created for each OpenTelemetry Span. Depending on + * the SpanKind type, this method may start a New Relic Java agent Transaction. + * + * @return OpenTelemetry Span + */ + @Override + public Span startSpan() { + SpanContext parentSpanContext = this.parentSpanContext == null ? + Span.fromContext(Context.current()).getSpanContext() : this.parentSpanContext; + if (SpanKind.SERVER == spanKind) { + return startServerSpan(parentSpanContext); + } + final boolean dispatcher = SpanKind.CONSUMER.equals(spanKind); + if (dispatcher) { + AgentBridge.getAgent().getTransaction(true); + } + final ExitTracer tracer = instrumentation.createTracer(spanName, getTracerFlags(dispatcher)); + if (tracer == null) { + return NO_OP_SPAN; + } + if (SpanKind.INTERNAL != spanKind) { + tracer.addCustomAttribute("span.kind", spanKind.name()); + } + // TODO REVIEW - we're not picking up the global resources + return onStart(new ExitTracerSpan(tracer, instrumentationLibraryInfo, spanKind, spanName, parentSpanContext, sharedState.getResource(), attributes, + endHandler)); + } + + private Span startServerSpan(SpanContext parentSpanContext) { + Transaction transaction = AgentBridge.getAgent().getTransaction(true); + final ExtendedRequest request = new ExtendedRequest() { + + @Override + public String getRequestURI() { + Object httpRoute = attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Route, attributes.keySet())); + return httpRoute == null ? null : httpRoute.toString(); + } + + @Override + public String getRemoteUser() { + return null; + } + + @Override + public Enumeration getParameterNames() { + return Collections.emptyEnumeration(); + } + + @Override + public String[] getParameterValues(String name) { + return new String[0]; + } + + @Override + public Object getAttribute(String name) { + return null; + } + + @Override + public String getCookieValue(String name) { + return null; + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + + @Override + public String getHeader(String name) { + if ("User-Agent".equalsIgnoreCase(name)) { + return (String) attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Host, attributes.keySet())); + } + // TODO is it possible to get the newrelic DT header from OTel??? + if (NEWRELIC.equalsIgnoreCase(name)) { + return null; + } + return null; + } + + @Override + public List getHeaders(String name) { + if (name.isEmpty()) { + return Collections.emptyList(); + } + List headers = new ArrayList<>(); + + if (W3C_TRACESTATE.equalsIgnoreCase(name)) { + Map traceState = parentSpanContext.getTraceState().asMap(); + StringBuilder tracestateStringBuilder = new StringBuilder(); + // Build full tracestate header incase there are multiple vendors + for (Map.Entry entry : traceState.entrySet()) { + if (tracestateStringBuilder.length() == 0) { + tracestateStringBuilder.append(entry.toString()); + } else { + tracestateStringBuilder.append(",").append(entry.toString()); + } + } + headers.add(tracestateStringBuilder.toString()); + return headers; + } + if (W3C_TRACEPARENT.equalsIgnoreCase(name)) { + String traceParent = W3CTraceParentHeader.create(parentSpanContext); + if (!traceParent.isEmpty()) { + headers.add(traceParent); + return headers; + } + } + return headers; + } + + @Override + public String getMethod() { + return (String) attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Method, attributes.keySet())); + } + }; + + final ExtendedResponse response = new ExtendedResponse() { + + @Override + public int getStatus() throws Exception { + Object statusCode = attributes.get(attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.StatusCode, attributes.keySet())); + return statusCode instanceof Number ? ((Number) statusCode).intValue() : 0; + } + + @Override + public String getStatusMessage() throws Exception { + return null; + } + + @Override + public String getContentType() { + return null; + } + + @Override + public HeaderType getHeaderType() { + return HeaderType.HTTP; + } + + @Override + public void setHeader(String name, String value) { + + } + + @Override + public long getContentLength() { + return 0; + } + }; + transaction.requestInitialized(request, response); + TracedMethod tracedMethod = transaction.getTracedMethod(); + return onStart(new ExitTracerSpan((ExitTracer) tracedMethod, instrumentationLibraryInfo, spanKind, spanName, + parentSpanContext, sharedState.getResource(), attributes, endHandler)); + } + + Span onStart(ReadWriteSpan span) { + // FIXME + Context parent = Context.current(); + if (sharedState.getActiveSpanProcessor().isStartRequired()) { + sharedState.getActiveSpanProcessor().onStart(parent, span); + } + return span; + } + + static int getTracerFlags(boolean dispatcher) { + int flags = TracerFlags.GENERATE_SCOPED_METRIC + | TracerFlags.TRANSACTION_TRACER_SEGMENT + | TracerFlags.CUSTOM; + if (dispatcher) { + flags |= TracerFlags.DISPATCHER; + } + return flags; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRTracerBuilder.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRTracerBuilder.java new file mode 100644 index 0000000000..ed6fb6f685 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRTracerBuilder.java @@ -0,0 +1,57 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.agent.bridge.AgentBridge; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerBuilder; + +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.isOpenTelemetryTracerDisabled; + +/** + * New Relic Java agent implementation of an OpenTelemetry + * TracerBuilder, which is a factory for building OpenTelemetry Tracers. + * An OpenTelemetry Tracer can then be used to create OpenTelemetry Spans. + */ +class NRTracerBuilder implements TracerBuilder { + private final String instrumentationScopeName; + private final TracerSharedState sharedState; + private String schemaUrl; + private String instrumentationScopeVersion; + + public NRTracerBuilder(String instrumentationScopeName, TracerSharedState sharedState) { + this.instrumentationScopeName = instrumentationScopeName; + this.sharedState = sharedState; + } + + @Override + public TracerBuilder setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + @Override + public TracerBuilder setInstrumentationVersion(String instrumentationScopeVersion) { + this.instrumentationScopeVersion = instrumentationScopeVersion; + return this; + } + + /** + * Builds a new OpenTelemetry Tracer. + * If the OTel Tracer is disabled in the configuration, returns a noop Tracer. + */ + @Override + public Tracer build() { + if (isOpenTelemetryTracerDisabled(instrumentationScopeName)) { + return OpenTelemetry.noop().getTracer(instrumentationScopeName); + } else { + return spanName -> new NRSpanBuilder(AgentBridge.instrumentation, instrumentationScopeName, instrumentationScopeVersion, sharedState, spanName); + } + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider_Instrumentation.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider_Instrumentation.java new file mode 100644 index 0000000000..c29ccf25e4 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider_Instrumentation.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import io.opentelemetry.api.trace.TracerBuilder; + +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.isOpenTelemetryTracesEnabled; + +/** + * Weaved to inject a New Relic Java agent implementation of an OpenTelemetry TracerBuilder + */ +@Weave(type = MatchType.ExactClass, originalName = "io.opentelemetry.sdk.trace.SdkTracerProvider") +public final class SdkTracerProvider_Instrumentation { + private final TracerSharedState sharedState = Weaver.callOriginal(); + + public TracerBuilder tracerBuilder(String instrumentationScopeName) { + final TracerBuilder tracerBuilder = Weaver.callOriginal(); + if (isOpenTelemetryTracesEnabled()) { + // Generate the instrumentation module enabled supportability metric + NewRelic.incrementCounter("Supportability/Tracing/Java/OpenTelemetryBridge/enabled"); + // return our tracer builder instead of the OTel instance + return new NRTracerBuilder(instrumentationScopeName, sharedState); + } else { + // Generate the instrumentation module disabled supportability metric + NewRelic.incrementCounter("Supportability/Tracing/Java/OpenTelemetryBridge/disabled"); + } + return tracerBuilder; + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/resources/attribute-mappings.json b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/resources/attribute-mappings.json new file mode 100644 index 0000000000..c6a2e4d628 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/resources/attribute-mappings.json @@ -0,0 +1,389 @@ +[ + { + "spanKind": "SERVER", + "attributeTypes": [ + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.port", + "version": "HTTP-Server:1.20" + } + ] + }, + { + "attributeType": "Host", + "attributes": [ + { + "name": "server.address", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.name", + "version": "HTTP-Server:1.20" + }, + { + "name": "user_agent.original", + "version": "" + } + ] + }, + { + "attributeType": "StatusCode", + "attributes": [ + { + "name": "http.response.status_code", + "version": "HTTP-Server:1.23" + }, + { + "name": "http.status_code", + "version": "HTTP-Server:1.20" + }, + { + "name": "rpc.grpc.status_code", + "version": "RPC-Server:1.20" + } + ] + }, + { + "attributeType": "Method", + "attributes": [ + { + "name": "http.request.method", + "version": "HTTP-Server:1.23" + }, + { + "name": "http.method", + "version": "HTTP-Server:1.20" + }, + { + "name": "rpc.method", + "version": "RPC-Server:1.20" + } + ] + }, + { + "attributeType": "ExternalProcedure", + "attributes": [ + { + "name": "code.function", + "version": "" + } + ] + }, + { + "attributeType": "Route", + "attributes": [ + { + "name": "http.route", + "version": "HTTP-Server:1.23,HTTP-Server:1.20" + }, + { + "name": "url.path", + "version": "" + }, + { + "name": "url.full", + "version": "" + }, + { + "name": "url.scheme", + "version": "" + } + ] + }, + { + "attributeType": "Component", + "attributes": [ + { + "name": "rpc.system", + "version": "RPC-Server:1.20" + } + ] + } + ] + }, + { + "spanKind": "CONSUMER", + "attributeTypes": [ + { + "attributeType": "Queue", + "attributes": [ + { + "name": "messaging.destination.name", + "version": "Messaging-Consumer-1.24" + }, + { + "name": "messaging.destination", + "version": "Messaging-Consumer-1.17" + } + ] + }, + { + "attributeType": "Host", + "attributes": [ + { + "name": "server.address", + "version": "Messaging-Consumer-1.24" + }, + { + "name": "net.peer.name", + "version": "Messaging-Consumer-1.17" + } + ] + }, + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "Messaging-Consumer-1.24" + }, + { + "name": "net.peer.port", + "version": "Messaging-Consumer-1.17" + } + ] + }, + { + "attributeType": "RoutingKey", + "attributes": [ + { + "name": "messaging.kafka.message.key", + "version": "Messaging-Consumer-1.24" + }, + { + "name": "messaging.rabbitmq.destination.routing_key", + "version": "Messaging-Consumer-1.24,Messaging-Consumer-1.17" + } + ] + } + ] + }, + { + "spanKind": "CLIENT", + "attributeTypes": [ + { + "attributeType": "Route", + "attributes": [ + { + "name": "http.route", + "version": "HTTP-Server:1.23,HTTP-Server:1.20" + }, + { + "name": "url.path", + "version": "" + }, + { + "name": "url.full", + "version": "" + }, + { + "name": "url.scheme", + "version": "" + } + ] + }, + { + "attributeType": "DBName", + "attributes": [ + { + "name": "db.name", + "version": "Redis-Client:1.17,Mongo-Client:1.24,Mongo-Client:1.17,DynamoDB-Client:1.17,DB-Client:1.24,DB-Client:1.17" + } + ] + }, + { + "attributeType": "DBOperation", + "attributes": [ + { + "name": "db.operation", + "version": "Redis-Client:1.17,Mongo-Client:1.24,Mongo-Client:1.17,DynamoDB-Client:1.17,DB-Client:1.24,DB-Client:1.17" + } + ] + }, + { + "attributeType": "DBSystem", + "attributes": [ + { + "name": "db.system", + "version": "Redis-Client:1.24,Redis-Client:1.17,Mongo-Client:1.24,Mongo-Client:1.17,DynamoDB-Client:1.17,DB-Client:1.24,DB-Client:1.17" + } + ] + }, + { + "attributeType": "DBStatement", + "attributes": [ + { + "name": "db.statement", + "version": "Redis-Client:1.24,Redis-Client:1.17,Mongo-Client:1.24,Mongo-Client:1.17,DynamoDB-Client:1.17,DB-Client:1.24,DB-Client:1.17" + } + ] + }, + { + "attributeType": "DBTable", + "attributes": [ + { + "name": "db.sql.table", + "version": "Redis-Client:1.24,Redis-Client:1.17,Mongo-Client:1.24,Mongo-Client:1.17,DynamoDB-Client:1.17,DB-Client:1.24,DB-Client:1.17" + } + ] + }, + { + "attributeType": "Host", + "attributes": [ + { + "name": "server.address", + "version": "Redis-Client:1.24,Mongo-Client:1.24,Mongo-Client:1.24,DB-Client:1.24,HTTP-Client:1.17,RPC-Client:1.23" + }, + { + "name": "net.peer.name", + "version": "Redis-Client:1.17,Mongo-Client:1.17,DB-Client:1.17,HTTP-Client:1.23,RPC-Client:1.17" + } + ] + }, + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "Redis-Client:1.24,Mongo-Client:1.24,DB-Client:1.24,HTTP-Client:1.17,RPC-Client:1.23" + }, + { + "name": "net.peer.port", + "version": "Redis-Client:1.17,Mongo-Client:1.17,DB-Client:1.17,HTTP-Client:1.23,RPC-Client:1.17" + } + ] + }, + { + "attributeType": "StatusCode", + "attributes": [ + { + "name": "http.status.code", + "version": "HTTP-Server:1.23,HTTP-Server:1.20,HTTP-Client:1.17" + }, + { + "name": "http.response.status_code", + "version": "HTTP-Client:1.23" + }, + { + "name": "rpc.grpc.status_code", + "version": "RPC-Client:1.17,RPC-Client:1.23" + } + ] + }, + { + "attributeType": "Method", + "attributes": [ + { + "name": "http.method", + "version": "HTTP-Client:1.17" + }, + { + "name": "http.request.method", + "version": "HTTP-Client:1.23" + }, + { + "name": "rpc.method", + "version": "RPC-Client:1.17,RPC-Client:1.17" + } + ] + }, + { + "attributeType": "ExternalProcedure", + "attributes": [ + { + "name": "code.function", + "version": "" + } + ] + }, + { + "attributeType": "Component", + "attributes": [ + { + "name": "rpc.system", + "version": "RPC-Client:1.17,RPC-Client:1.17" + } + ] + } + ] + }, + { + "spanKind": "PRODUCER", + "attributeTypes": [ + { + "attributeType": "Queue", + "attributes": [ + { + "name": "messaging.destination.name", + "version": "Messaging-Consumer-1.24" + }, + { + "name": "messaging.destination", + "version": "Messaging-Consumer-1.17" + }, + { + "name": "aws_sqs", + "version": "SQS-Producer-1.17" + }, + { + "name": "aws.region", + "version": "SQS-Producer-1.17" + } + ] + }, + { + "attributeType": "Host", + "attributes": [ + { + "name": "server.address", + "version": "Messaging-Producer-1.24,Messaging-Producer-1.30" + }, + { + "name": "net.peer.name", + "version": "Messaging-Producer-1.17" + } + ] + }, + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "Messaging-Producer-1.24,Messaging-Producer-1.30" + }, + { + "name": "net.peer.port", + "version": "Messaging-Producer-1.17" + } + ] + }, + { + "attributeType": "RoutingKey", + "attributes": [ + { + "name": "messaging.kafka.message.key", + "version": "Messaging-Producer-1.24,Messaging-Producer-1.30" + }, + { + "name": "messaging.rabbitmq.destination.routing_key", + "version": "Messaging-Producer-1.17,Messaging-Producer-1.24,Messaging-Producer-1.30" + }, + { + "name": "messaging.message.conversation_id", + "version": "Messaging-Producer-1.17,Messaging-Producer-1.24,Messaging-Producer-1.30" + }, + { + "name": "messaging.destination", + "version": "SQS-Producer-1.17" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/com/nr/agent/instrumentation/utils/config/OpenTelemetryConfigTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/com/nr/agent/instrumentation/utils/config/OpenTelemetryConfigTest.java new file mode 100644 index 0000000000..8df9dcdcda --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/com/nr/agent/instrumentation/utils/config/OpenTelemetryConfigTest.java @@ -0,0 +1,309 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.utils.config; + +import com.google.common.collect.ImmutableMap; +import com.newrelic.agent.config.SystemPropertyFactory; +import com.newrelic.agent.config.SystemPropertyProvider; +import com.newrelic.api.agent.Agent; +import com.newrelic.api.agent.NewRelic; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.List; +import java.util.Properties; + +import static com.newrelic.agent.config.SystemPropertyFactory.setSystemPropertyProvider; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.COMMA_SEPARATOR; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_ENABLED; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_ENABLED_DEFAULT; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_LOGS_ENABLED; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_LOGS_ENABLED_DEFAULT; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_METRICS_ENABLED; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_METRICS_ENABLED_DEFAULT; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_METRICS_EXCLUDE; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_METRICS_INCLUDE; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_TRACES_ENABLED; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_TRACES_ENABLED_DEFAULT; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_TRACES_EXCLUDE; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_TRACES_INCLUDE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static test.config.util.SaveSystemPropertyProviderRule.TestEnvironmentFacade; +import static test.config.util.SaveSystemPropertyProviderRule.TestSystemProps; + +public class OpenTelemetryConfigTest { + // Env vars + private static final String NEW_RELIC_OPENTELEMETRY_METRICS_EXCLUDE = "NEW_RELIC_OPENTELEMETRY_METRICS_EXCLUDE"; + private static final String NEW_RELIC_OPENTELEMETRY_TRACES_EXCLUDE = "NEW_RELIC_OPENTELEMETRY_TRACES_EXCLUDE"; + // Sys props + private static final String NEWRELIC_CONFIG_OPENTELEMETRY_METRICS_EXCLUDE = "newrelic.config.opentelemetry.metrics.exclude"; + private static final String NEWRELIC_CONFIG_OPENTELEMETRY_TRACES_EXCLUDE = "newrelic.config.opentelemetry.traces.exclude"; + + private Properties props; + private TestSystemProps testSystemProps; + private TestEnvironmentFacade environmentFacade; + private SystemPropertyProvider systemPropertyProvider; + + @Before + public void setUp() { + props = new Properties(); + testSystemProps = new TestSystemProps(); + environmentFacade = new TestEnvironmentFacade(); + systemPropertyProvider = SystemPropertyFactory.getSystemPropertyProvider(); + } + + @Test + public void testDefaultConfigValues() { + // By default, all Metrics includes/excludes are empty + assertTrue(OpenTelemetryConfig.getOpenTelemetryMetricsExcludes().isEmpty()); + assertTrue(OpenTelemetryConfig.getOpenTelemetryMetricsIncludes().isEmpty()); + + // By default, all Traces includes/excludes are empty + assertTrue(OpenTelemetryConfig.getOpenTelemetryTracesExcludes().isEmpty()); + assertTrue(OpenTelemetryConfig.getOpenTelemetryTracesIncludes().isEmpty()); + + // By default, all opentelemetry signals are disabled + assertFalse(OpenTelemetryConfig.isOpenTelemetryEnabled()); + assertFalse(OpenTelemetryConfig.isOpenTelemetrySdkAutoConfigureEnabled()); + assertFalse(OpenTelemetryConfig.isOpenTelemetryMetricsEnabled()); + assertFalse(OpenTelemetryConfig.isOpenTelemetryTracesEnabled()); + assertFalse(OpenTelemetryConfig.isOpenTelemetryLogsEnabled()); + } + + @Test + public void testGetOpenTelemetryMetricsExcludesFromYamlProps() { + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_METRICS_EXCLUDE, "")).thenReturn("foo,bar,baz"); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_METRICS_INCLUDE, "")).thenReturn(""); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + List openTelemetryMetricsExcludes = OpenTelemetryConfig.getOpenTelemetryMetricsExcludes(); + assertEquals(3, openTelemetryMetricsExcludes.size()); + assertTrue(openTelemetryMetricsExcludes.contains("foo")); + assertTrue(openTelemetryMetricsExcludes.contains("bar")); + assertTrue(openTelemetryMetricsExcludes.contains("baz")); + } + } + + @Test + public void testGetOpenTelemetryTracesExcludesFromYamlProps() { + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_EXCLUDE, "")).thenReturn("foo,bar,baz"); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_INCLUDE, "")).thenReturn(""); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + List openTelemetryTracesExcludes = OpenTelemetryConfig.getOpenTelemetryTracesExcludes(); + assertEquals(3, openTelemetryTracesExcludes.size()); + assertTrue(openTelemetryTracesExcludes.contains("foo")); + assertTrue(openTelemetryTracesExcludes.contains("bar")); + assertTrue(openTelemetryTracesExcludes.contains("baz")); + } + } + + @Test + public void testGetOpenTelemetryMetricsExcludesFromEnvVars() { + environmentFacade = new TestEnvironmentFacade(ImmutableMap.of( + NEW_RELIC_OPENTELEMETRY_METRICS_EXCLUDE, "hello,foo-bar,goodbye" + )); + systemPropertyProvider = new SystemPropertyProvider( + testSystemProps, + environmentFacade + ); + setSystemPropertyProvider(systemPropertyProvider); + + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_METRICS_EXCLUDE, "")).thenReturn(environmentFacade.getenv( + NEW_RELIC_OPENTELEMETRY_METRICS_EXCLUDE)); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_METRICS_INCLUDE, "")).thenReturn(""); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + List openTelemetryMetricsExcludes = OpenTelemetryConfig.getOpenTelemetryMetricsExcludes(); + assertEquals(3, openTelemetryMetricsExcludes.size()); + assertTrue(openTelemetryMetricsExcludes.contains("hello")); + assertTrue(openTelemetryMetricsExcludes.contains("foo-bar")); + assertTrue(openTelemetryMetricsExcludes.contains("goodbye")); + } + } + + @Test + public void testGetOpenTelemetryTracesExcludesFromEnvVars() { + environmentFacade = new TestEnvironmentFacade(ImmutableMap.of( + NEW_RELIC_OPENTELEMETRY_TRACES_EXCLUDE, "hello,bar-baz,goodbye,foo" + )); + systemPropertyProvider = new SystemPropertyProvider( + testSystemProps, + environmentFacade + ); + setSystemPropertyProvider(systemPropertyProvider); + + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_EXCLUDE, "")).thenReturn(environmentFacade.getenv( + NEW_RELIC_OPENTELEMETRY_TRACES_EXCLUDE)); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_INCLUDE, "")).thenReturn(""); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + List openTelemetryTracesExcludes = OpenTelemetryConfig.getOpenTelemetryTracesExcludes(); + assertEquals(4, openTelemetryTracesExcludes.size()); + assertTrue(openTelemetryTracesExcludes.contains("hello")); + assertTrue(openTelemetryTracesExcludes.contains("bar-baz")); + assertTrue(openTelemetryTracesExcludes.contains("goodbye")); + assertTrue(openTelemetryTracesExcludes.contains("foo")); + } + } + + @Test + public void testExcludeMetersFromSystemProps() { + props.put(NEWRELIC_CONFIG_OPENTELEMETRY_METRICS_EXCLUDE, "apple,banana"); + testSystemProps = new TestSystemProps(props); + systemPropertyProvider = new SystemPropertyProvider( + testSystemProps, + environmentFacade + ); + setSystemPropertyProvider(systemPropertyProvider); + + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_METRICS_EXCLUDE, "")).thenReturn(testSystemProps.getSystemProperty( + NEWRELIC_CONFIG_OPENTELEMETRY_METRICS_EXCLUDE)); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_METRICS_INCLUDE, "")).thenReturn(""); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + List openTelemetryMetricsExcludes = OpenTelemetryConfig.getOpenTelemetryMetricsExcludes(); + assertEquals(2, openTelemetryMetricsExcludes.size()); + assertTrue(openTelemetryMetricsExcludes.contains("apple")); + assertTrue(openTelemetryMetricsExcludes.contains("banana")); + } + } + + @Test + public void testExcludeTracesFromSystemProps() { + props.put(NEWRELIC_CONFIG_OPENTELEMETRY_TRACES_EXCLUDE, "apple,banana,boat"); + testSystemProps = new TestSystemProps(props); + systemPropertyProvider = new SystemPropertyProvider( + testSystemProps, + environmentFacade + ); + setSystemPropertyProvider(systemPropertyProvider); + + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_EXCLUDE, "")).thenReturn(testSystemProps.getSystemProperty( + NEWRELIC_CONFIG_OPENTELEMETRY_TRACES_EXCLUDE)); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_INCLUDE, "")).thenReturn(""); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + List openTelemetryTracesExcludes = OpenTelemetryConfig.getOpenTelemetryTracesExcludes(); + assertEquals(3, openTelemetryTracesExcludes.size()); + assertTrue(openTelemetryTracesExcludes.contains("apple")); + assertTrue(openTelemetryTracesExcludes.contains("banana")); + assertTrue(openTelemetryTracesExcludes.contains("boat")); + } + } + + @Test + public void testIsOpenTelemetryTracerDisabled() { + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_EXCLUDE, "")).thenReturn("foo"); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_INCLUDE, "")).thenReturn(""); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + // The 'foo' tracer is disabled because it is in the exclude list + assertTrue(OpenTelemetryConfig.isOpenTelemetryTracerDisabled("foo")); + // The 'bar' tracer is enabled + assertFalse(OpenTelemetryConfig.isOpenTelemetryTracerDisabled("bar")); + } + } + + @Test + public void testGetUniqueStringsFromString() { + List splitStrings = OpenTelemetryConfig.getUniqueStringsFromString("one,two, three , four, five,six", COMMA_SEPARATOR); + assertEquals(6, splitStrings.size()); + assertTrue(splitStrings.contains("one")); + assertTrue(splitStrings.contains("two")); + assertTrue(splitStrings.contains("three")); + assertTrue(splitStrings.contains("four")); + assertTrue(splitStrings.contains("five")); + assertTrue(splitStrings.contains("six")); + } + + @Test + public void testOpenTelemetryEnabled() { + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_ENABLED, OPENTELEMETRY_ENABLED_DEFAULT)).thenReturn(true); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + assertTrue(OpenTelemetryConfig.isOpenTelemetryEnabled()); + } + } + + @Test + public void testOpenTelemetryEnabledFalse() { + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_ENABLED, OPENTELEMETRY_ENABLED_DEFAULT)).thenReturn(false); + // Set the individual logs, metrics, and traces signals as enabled + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_LOGS_ENABLED, OPENTELEMETRY_LOGS_ENABLED_DEFAULT)).thenReturn(true); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_METRICS_ENABLED, OPENTELEMETRY_METRICS_ENABLED_DEFAULT)).thenReturn(true); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_ENABLED, OPENTELEMETRY_TRACES_ENABLED_DEFAULT)).thenReturn(true); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + assertFalse(OpenTelemetryConfig.isOpenTelemetryEnabled()); + // The individual logs, metrics, and traces signals will evaluate as disabled since opentelemetry.enabled is false + assertFalse(OpenTelemetryConfig.isOpenTelemetryLogsEnabled()); + assertFalse(OpenTelemetryConfig.isOpenTelemetryMetricsEnabled()); + assertFalse(OpenTelemetryConfig.isOpenTelemetryTracesEnabled()); + } + } + + @Test + public void testOpenTelemetryLogsEnabled() { + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_ENABLED, OPENTELEMETRY_ENABLED_DEFAULT)).thenReturn(true); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_LOGS_ENABLED, OPENTELEMETRY_LOGS_ENABLED_DEFAULT)).thenReturn(true); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + assertTrue(OpenTelemetryConfig.isOpenTelemetryLogsEnabled()); + } + } + + @Test + public void testOpenTelemetryMetricsEnabled() { + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_ENABLED, OPENTELEMETRY_ENABLED_DEFAULT)).thenReturn(true); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_METRICS_ENABLED, OPENTELEMETRY_METRICS_ENABLED_DEFAULT)).thenReturn(true); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + assertTrue(OpenTelemetryConfig.isOpenTelemetryMetricsEnabled()); + } + } + + @Test + public void testOpenTelemetryTracesEnabled() { + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_ENABLED, OPENTELEMETRY_ENABLED_DEFAULT)).thenReturn(true); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_ENABLED, OPENTELEMETRY_TRACES_ENABLED_DEFAULT)).thenReturn(true); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + assertTrue(OpenTelemetryConfig.isOpenTelemetryTracesEnabled()); + } + } + +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/com/nr/agent/instrumentation/utils/span/AttributeMapperTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/com/nr/agent/instrumentation/utils/span/AttributeMapperTest.java new file mode 100644 index 0000000000..d359cd1018 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/com/nr/agent/instrumentation/utils/span/AttributeMapperTest.java @@ -0,0 +1,70 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation.utils.span; + +import io.opentelemetry.api.trace.SpanKind; +import org.junit.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +public class AttributeMapperTest { + @Test + public void getInstance_withValidJson_createsValidMapper() { + AttributeMapper attributeMapper = AttributeMapper.getInstance(); + + // 5 SpanKind types + Map>> mappings = attributeMapper.getMappings(); + assertEquals(5, mappings.size()); + + for (SpanKind spanKind : SpanKind.values()) { + Map> attributeTypesBySpan = mappings.get(spanKind); + assertEquals(14, attributeTypesBySpan.size()); + } + } + + @Test + public void findProperOtelKey_returnsProperKey() { + AttributeMapper attributeMapper = AttributeMapper.getInstance(); + Set otelKeys = new HashSet<>(); + otelKeys.add("key1"); + otelKeys.add("key2"); + otelKeys.add("key3"); + otelKeys.add("server.port"); //Should find this in the mapper + + assertEquals("server.port", attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Port, otelKeys)); + } + + @Test + public void findProperOtelKey_returnsEmptyString_whenRequestedKeyNotFound() { + AttributeMapper attributeMapper = AttributeMapper.getInstance(); + Set otelKeys = new HashSet<>(); + otelKeys.add("key1"); + otelKeys.add("key2"); + otelKeys.add("key3"); + + assertEquals("", attributeMapper.findProperOtelKey(SpanKind.SERVER, AttributeType.Port, otelKeys)); + } + + @Test + public void attributeKeyClass_properlyParsesSemanticConventionField() { + AttributeMapper attributeMapper = AttributeMapper.getInstance(); + + Map>> attributes = attributeMapper.getMappings(); + AttributeKey attribute = attributes.get(SpanKind.SERVER).get(AttributeType.Port).get(0); + assertEquals(1, attribute.getSemanticConventions().length); + assertEquals("HTTP-Server:1.23", attribute.getSemanticConventions()[0]); + + attribute = attributes.get(SpanKind.SERVER).get(AttributeType.Host).get(0); + assertEquals(1, attribute.getSemanticConventions().length); + assertEquals("HTTP-Server:1.23", attribute.getSemanticConventions()[0]); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/agent/otelhybrid/AssertionEvaluator.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/agent/otelhybrid/AssertionEvaluator.java new file mode 100644 index 0000000000..e4f47c2e91 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/agent/otelhybrid/AssertionEvaluator.java @@ -0,0 +1,84 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package io.opentelemetry.agent.otelhybrid; + +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.SpanEvent; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +import static io.opentelemetry.agent.otelhybrid.HybridAgentTest.PARENT_SPAN_ID; +import static io.opentelemetry.agent.otelhybrid.HybridAgentTest.PARENT_TRACE_ID; +import static io.opentelemetry.agent.otelhybrid.HybridAgentTest.SPAN_ID; +import static io.opentelemetry.agent.otelhybrid.HybridAgentTest.TRACE_ID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class AssertionEvaluator { + public static void assertNoTxnExists(Introspector introspector) { + assertEquals(0, introspector.getFinishedTransactionCount()); + } + + public static void assertNoNewRelicSpanExists(Introspector introspector) { + assertEquals(0, introspector.getSpanEvents().size()); + } + + public static void assertSpanCount(Introspector introspector, int count) { + assertEquals(count, introspector.getSpanEvents().size()); + } + + public static void assertSpanDetails(Introspector introspector, Map otelSpanDetails) { + Collection spanEvents = introspector.getSpanEvents(); + if (!spanEvents.isEmpty() && !otelSpanDetails.isEmpty()) { + String otelSpanId = otelSpanDetails.get(SPAN_ID); + String otelSpanTraceId = otelSpanDetails.get(TRACE_ID); + String otelParentSpanId = otelSpanDetails.get(PARENT_SPAN_ID); + String otelParentTraceId = otelSpanDetails.get(PARENT_TRACE_ID); + + if (otelSpanId != null && otelSpanTraceId != null) { + Optional spanEventOptional = spanEvents.stream().filter(span -> otelSpanId.equals(span.getGuid())).findFirst(); + if (spanEventOptional.isPresent()) { + SpanEvent nrSpan = spanEventOptional.get(); + // OpenTelemetry API and New Relic API report the same traceId + assertEquals(otelSpanTraceId, nrSpan.traceId()); + // OpenTelemetry API and New Relic API report the same spanId + assertEquals(otelSpanId, nrSpan.getGuid()); + } + } + + if (otelParentSpanId != null && otelParentTraceId != null) { + Optional spanEventOptional = spanEvents.stream().filter(span -> otelParentSpanId.equals(span.getGuid())).findFirst(); + if (spanEventOptional.isPresent()) { + SpanEvent nrParentSpan = spanEventOptional.get(); + // OpenTelemetry API and New Relic API report the same traceId + assertEquals(otelParentTraceId, nrParentSpan.traceId()); + // OpenTelemetry API and New Relic API report the same spanId + assertEquals(otelParentSpanId, nrParentSpan.getGuid()); + } + } + } + } + + public static void assertTxnExists(Introspector introspector, String name) { + assertEquals(name, introspector.getTransactionNames().iterator().next()); + } + + public static void assertUserAttributeExists(String key, Object val, Map attributes) { + assertEquals(attributes.get(key), val); + } + + public static void assertExceptionExistsOnSpan(SpanEvent spanEvent, String errorMessage, String errorClass) { + assertEquals(errorMessage, spanEvent.getAgentAttributes().get("error.message")); + assertEquals(errorClass, spanEvent.getAgentAttributes().get("error.class")); + } + + public static void assertCarrierContainsW3CTraceParent(Map carrier) { + assertNotNull(carrier.get("traceparent")); + } +} \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/agent/otelhybrid/HybridAgentTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/agent/otelhybrid/HybridAgentTest.java new file mode 100644 index 0000000000..33a68c3de0 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/agent/otelhybrid/HybridAgentTest.java @@ -0,0 +1,614 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package io.opentelemetry.agent.otelhybrid; + +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.SpanEvent; +import com.newrelic.agent.model.LogEvent; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Trace; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerBuilder; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.LogRecordProcessor; +import io.opentelemetry.sdk.logs.ReadWriteLogRecord; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.ExitTracerSpan; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.opentelemetry.sdk.logs.NRLogRecord.OTEL_LIBRARY_NAME; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "io.opentelemetry" }, configName = "distributed_tracing.yml") +public class HybridAgentTest { + static { + System.setProperty("otel.java.global-autoconfigure.enabled", "true"); + } + + private static final List EMITTED_LOG_RECORDS = new ArrayList<>(); + + private static final LogRecordProcessor LOG_RECORD_PROCESSOR = new LogRecordProcessor() { + @Override + public void onEmit(Context context, ReadWriteLogRecord logRecord) { + EMITTED_LOG_RECORDS.add(logRecord); + } + + @Override + public CompletableResultCode shutdown() { + return LogRecordProcessor.super.shutdown(); + } + + @Override + public CompletableResultCode forceFlush() { + return LogRecordProcessor.super.forceFlush(); + } + + @Override + public void close() { + LogRecordProcessor.super.close(); + } + }; + + private static final Attributes LOG_ATTRIBUTES = Attributes.builder() + .put("service.name", NewRelic.getAgent().getConfig().getValue("app_name", "unknown")) + .put("service.version", "4.5.1") + .put("environment", "production") + .build(); + + private static final Resource CUSTOM_RESOURCE = Resource.create(LOG_ATTRIBUTES); + + private static final SdkLoggerProvider SDK_LOGGER_PROVIDER = SdkLoggerProvider.builder().addLogRecordProcessor(LOG_RECORD_PROCESSOR).setResource( + CUSTOM_RESOURCE).build(); + + private static final String INSTRUMENTATION_SCOPE_NAME = "test"; + + private static final LoggerBuilder LOGGER_BUILDER = SDK_LOGGER_PROVIDER + .loggerBuilder(INSTRUMENTATION_SCOPE_NAME) + .setInstrumentationVersion("1.0.0") + .setSchemaUrl("https://opentelemetry.io/schemas/1.0.0"); + + private static final Logger LOGGER = LOGGER_BUILDER.build(); + + static final Tracer OTEL_TRACER = GlobalOpenTelemetry.get().getTracer(INSTRUMENTATION_SCOPE_NAME, "1.0.0"); + public static final String SPAN_ID = "spanId"; + public static final String TRACE_ID = "traceId"; + public static final String PARENT_SPAN_ID = "parentSpanId"; + public static final String PARENT_TRACE_ID = "parentTraceId"; + + // OpenTelemetry API and New Relic API can inject outbound trace context + @Test + public void doesNotCreateSegmentWithoutATransaction() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map spanDetails = doWorkInSpanWithoutTxn("Bar", SpanKind.INTERNAL); + + AssertionEvaluator.assertNoTxnExists(introspector); + AssertionEvaluator.assertNoNewRelicSpanExists(introspector); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // Starting transaction tests + @Test + public void createTxnWhenServerSpanCreated() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map spanDetails = createTransactionWhenServerSpanCreated("Foo"); + + AssertionEvaluator.assertTxnExists(introspector, "WebTransaction/Uri/Unknown"); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // Starting transaction tests + @Test + public void createTxnWhenServerSpanCreatedFromRemoteParent() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map spanDetails = createTransactionWhenServerSpanCreatedFromRemoteContext("Bar"); + + AssertionEvaluator.assertSpanCount(introspector, 1); + AssertionEvaluator.assertTxnExists(introspector, "WebTransaction/Uri/Unknown"); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // Starting transaction tests + @Test + public void createTxnWithServerSpanCreatedFromRemoteParent() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map spanDetails = createTransactionWithServerSpanCreatedFromRemoteContext("EdgeCase"); + + AssertionEvaluator.assertSpanCount(introspector, 1); + AssertionEvaluator.assertTxnExists(introspector, + "OtherTransaction/Custom/io.opentelemetry.agent.otelhybrid.HybridAgentTest/createTransactionWithServerSpanCreatedFromRemoteContext"); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // Creates OpenTelemetry segment in a transaction + @Test + public void createsOtelSegmentInTxn() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map spanDetails = createOtelSegmentInTxn("Foo", SpanKind.INTERNAL); + + AssertionEvaluator.assertSpanCount(introspector, 2); + AssertionEvaluator.assertTxnExists(introspector, "OtherTransaction/Custom/io.opentelemetry.agent.otelhybrid.HybridAgentTest/createOtelSegmentInTxn"); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // Creates New Relic span as child of OpenTelemetry span + @Test + public void createsNewRelicSpanAsChildOfOtelSpan() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map spanDetails = newRelicSpanAsChildOfOtelSpan("foo", SpanKind.INTERNAL); + + AssertionEvaluator.assertSpanCount(introspector, 3); + AssertionEvaluator.assertTxnExists(introspector, + "OtherTransaction/Custom/io.opentelemetry.agent.otelhybrid.HybridAgentTest/newRelicSpanAsChildOfOtelSpan"); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // OpenTelemetry API can add custom attributes to spans + @Test + public void otelSpansCanAddAttributes() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map spanDetails = addAttributesToOtelSpan("foo", SpanKind.INTERNAL); + + AssertionEvaluator.assertSpanCount(introspector, 2); + AssertionEvaluator.assertTxnExists(introspector, "OtherTransaction/Custom/io.opentelemetry.agent.otelhybrid.HybridAgentTest/addAttributesToOtelSpan"); + + Collection spanEvents = introspector.getSpanEvents(); + SpanEvent[] eventArray = spanEvents.toArray(new SpanEvent[0]); + AssertionEvaluator.assertUserAttributeExists("key1", "val1", eventArray[1].getUserAttributes()); + AssertionEvaluator.assertUserAttributeExists("key2", "val2", eventArray[1].getUserAttributes()); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // OpenTelemetry API can record errors + @Test + public void exceptionsAreRecordedOnOtelSpan() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map spanDetails = otelSpanRecordsException("foo", SpanKind.INTERNAL); + + AssertionEvaluator.assertSpanCount(introspector, 2); + AssertionEvaluator.assertTxnExists(introspector, "OtherTransaction/Custom/io.opentelemetry.agent.otelhybrid.HybridAgentTest/otelSpanRecordsException"); + + Collection spanEvents = introspector.getSpanEvents(); + SpanEvent[] eventArray = spanEvents.toArray(new SpanEvent[0]); + AssertionEvaluator.assertExceptionExistsOnSpan(eventArray[1], "oops", "java.lang.Exception"); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // Inbound distributed tracing tests + @Test + public void externalCallWithW3CHeaderInjection() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map> mapOfMaps = externalCallInjectsW3CHeaders("foo", SpanKind.CLIENT); + + Map carrier = mapOfMaps.get("carrier"); + Map spanDetails = mapOfMaps.get("spanDetails"); + + AssertionEvaluator.assertSpanCount(introspector, 2); + AssertionEvaluator.assertTxnExists(introspector, + "OtherTransaction/Custom/io.opentelemetry.agent.otelhybrid.HybridAgentTest/externalCallInjectsW3CHeaders"); + AssertionEvaluator.assertCarrierContainsW3CTraceParent(carrier); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // Inbound distributed tracing tests + @Test + public void distributedTracingSpanWithInboundContext() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map> mapOfMaps = doWorkInSpanWithInboundContext("foo", SpanKind.SERVER); + + Map carrier = mapOfMaps.get("carrier"); + Map spanDetails = mapOfMaps.get("spanDetails"); + + AssertionEvaluator.assertSpanCount(introspector, 1); + AssertionEvaluator.assertTxnExists(introspector, "WebTransaction/Uri/Unknown"); + AssertionEvaluator.assertCarrierContainsW3CTraceParent(carrier); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // OpenTelemetry API and New Relic API can inject outbound trace context + @Test + public void apisInjectOutboundTraceContext() { + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Map spanDetails = testApisInTxn(); + + AssertionEvaluator.assertTxnExists(introspector, + "OtherTransaction/Custom/io.opentelemetry.agent.otelhybrid.HybridAgentTest/testApisInTxn"); + AssertionEvaluator.assertSpanCount(introspector, 3); + AssertionEvaluator.assertSpanDetails(introspector, spanDetails); + } + + // OpenTelemetry Log API will create a LogEvent associated with a transaction + @Test + public void testOtelLogRecordInTxn() { + Map linkingMetadata = emitOtelLogRecordInTxn(null); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Collection logEvents = introspector.getLogEvents(); + assertFalse(logEvents.isEmpty()); + assertFalse(EMITTED_LOG_RECORDS.isEmpty()); + assertEquals(EMITTED_LOG_RECORDS.size(), logEvents.size()); + + LogEvent logEvent = logEvents.iterator().next(); + assertEquals(INSTRUMENTATION_SCOPE_NAME, logEvent.getUserAttributesCopy().get(OTEL_LIBRARY_NAME.getKey())); + + ReadWriteLogRecord readWriteLogRecord = EMITTED_LOG_RECORDS.get(0); + LogRecordData logRecordData = readWriteLogRecord.toLogRecordData(); + SpanContext spanContext = logRecordData.getSpanContext(); + + // The trace.id and span.id from the linking metadata + // should be the same as what is on the SpanContext. + assertEquals(spanContext.getTraceId(), linkingMetadata.get("trace.id")); + assertEquals(spanContext.getSpanId(), linkingMetadata.get("span.id")); + + introspector.clearLogEvents(); + } + + // OpenTelemetry Log API will create a LogEvent outside a transaction + @Test + public void testOtelLogRecordNoTxn() { + Map linkingMetadata = emitOtelLogRecordNoTxn(null); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Collection logEvents = introspector.getLogEvents(); + assertFalse(logEvents.isEmpty()); + assertEquals(1, logEvents.size()); + + LogEvent logEvent = logEvents.iterator().next(); + assertEquals(INSTRUMENTATION_SCOPE_NAME, logEvent.getUserAttributesCopy().get(OTEL_LIBRARY_NAME.getKey())); + + ReadWriteLogRecord readWriteLogRecord = EMITTED_LOG_RECORDS.get(0); + LogRecordData logRecordData = readWriteLogRecord.toLogRecordData(); + SpanContext spanContext = logRecordData.getSpanContext(); + + // The trace.id and span.id from the linking metadata should be empty + // strings, while the SpanContext should be represented by all zeros. + assertNotEquals(spanContext.getTraceId(), linkingMetadata.get("trace.id")); + assertNotEquals(spanContext.getSpanId(), linkingMetadata.get("span.id")); + + introspector.clearLogEvents(); + } + + @Trace(dispatcher = true) + static Map testApisInTxn() { + Span span = OTEL_TRACER.spanBuilder("OTelSpan1").setSpanKind(SpanKind.CLIENT).startSpan(); + Map spanDetails = new HashMap<>(); + try (Scope scope = span.makeCurrent()) { + + Map fakeExternalHeaders = new HashMap<>(); + final TextMapPropagator propagator = W3CTraceContextPropagator.getInstance(); + TextMapSetter> setter = (carrier1, key, value) -> carrier1.put(key, value); + Context context = Context.current().with(span); + propagator.inject(context, fakeExternalHeaders, setter); + + // extract inbound trace context and make it the current scope + Context extractedContext = propagator.extract(Context.current(), fakeExternalHeaders, getter); + + try (Scope extractedScope = extractedContext.makeCurrent()) { + // create a new span using the extracted context/scope + Span fooSpan = OTEL_TRACER.spanBuilder("foo").setSpanKind(SpanKind.CLIENT).startSpan(); + Context fooContext = Context.current().with(span); + try (Scope fooScope = span.makeCurrent()) { + spanDetails = createOTelSpanDetailsMap(fooSpan); + } catch (Throwable t) { + fooSpan.recordException(t); + } finally { + fooSpan.end(); + } + } catch (Throwable ignored) { + } + } catch (Throwable t) { + span.recordException(t); + } finally { + span.end(); + } + + return spanDetails; + } + + static Map createOTelSpanDetailsMap(Span span) { + Map spanDetails = new HashMap<>(); + if (span instanceof ExitTracerSpan) { + SpanContext spanContext = span.getSpanContext(); + if (spanContext != null && spanContext.isValid()) { + spanDetails.put(SPAN_ID, spanContext.getSpanId()); + spanDetails.put(TRACE_ID, spanContext.getTraceId()); + } + + Span parentSpan = Span.wrap(((ExitTracerSpan) span).getParentSpanContext()); + if (parentSpan != null) { + SpanContext parentSpanContext = parentSpan.getSpanContext(); + if (parentSpanContext != null && parentSpanContext.isValid()) { + spanDetails.put(PARENT_SPAN_ID, parentSpanContext.getSpanId()); + spanDetails.put(PARENT_TRACE_ID, parentSpanContext.getTraceId()); + } + } + } + return spanDetails; + } + + static Map doWorkInSpanWithoutTxn(String spanName, SpanKind spanKind) { + Span span = OTEL_TRACER.spanBuilder(spanName).setSpanKind(spanKind).startSpan(); + Scope scope = span.makeCurrent(); + assertFalse(span.getSpanContext().isValid()); + + Map spanDetails = createOTelSpanDetailsMap(span); + + scope.close(); + span.end(); + + return spanDetails; + } + + @Trace(dispatcher = true) + static Map createOtelSegmentInTxn(String spanName, SpanKind spanKind) { + Span span = OTEL_TRACER.spanBuilder(spanName).setSpanKind(spanKind).startSpan(); + Scope scope = span.makeCurrent(); + + Map spanDetails = createOTelSpanDetailsMap(span); + + scope.close(); + span.end(); + + return spanDetails; + } + + @Trace(dispatcher = true) + static Map newRelicSpanAsChildOfOtelSpan(String spanName, SpanKind spanKind) { + Span span = OTEL_TRACER.spanBuilder(spanName).setSpanKind(spanKind).startSpan(); + Scope scope = span.makeCurrent(); + + newRelicWorkTracer(); + + Map spanDetails = createOTelSpanDetailsMap(span); + + scope.close(); + span.end(); + + return spanDetails; + } + + @Trace + static void newRelicWorkTracer() { + // Do something + } + + @Trace(dispatcher = true) + static Map addAttributesToOtelSpan(String spanName, SpanKind spanKind) { + Span span = OTEL_TRACER.spanBuilder(spanName).setSpanKind(spanKind).startSpan(); + Scope scope = span.makeCurrent(); + + span.setAttribute("key1", "val1"); + span.setAttribute("key2", "val2"); + + Map spanDetails = createOTelSpanDetailsMap(span); + + scope.close(); + span.end(); + + return spanDetails; + } + + @Trace(dispatcher = true) + static Map otelSpanRecordsException(String spanName, SpanKind spanKind) { + Span span = OTEL_TRACER.spanBuilder(spanName).setSpanKind(spanKind).startSpan(); + Scope scope = span.makeCurrent(); + + Map spanDetails = createOTelSpanDetailsMap(span); + + try { + throw new Exception("oops"); + } catch (Exception e) { + span.recordException(e); + span.setAttribute(AttributeKey.stringKey("error.type"), e.getClass().getCanonicalName()); + } finally { + scope.close(); + span.end(); + } + + return spanDetails; + } + + @Trace(dispatcher = true) + static Map> externalCallInjectsW3CHeaders(String spanName, SpanKind spanKind) { + final TextMapPropagator propagator = W3CTraceContextPropagator.getInstance(); + Span span = OTEL_TRACER.spanBuilder(spanName).setSpanKind(spanKind).startSpan(); + Context context = Context.current().with(span); + Scope scope = span.makeCurrent(); + + Map carrier = new HashMap<>(); + TextMapSetter> setter = (carrier1, key, value) -> carrier1.put(key, value); + propagator.inject(context, carrier, setter); + + Map spanDetails = createOTelSpanDetailsMap(span); + + Map> mapOfMaps = new HashMap<>(); + mapOfMaps.put("carrier", carrier); + mapOfMaps.put("spanDetails", spanDetails); + + scope.close(); + span.end(); + + return mapOfMaps; + } + + static Map createTransactionWhenServerSpanCreated(String spanName) { + Span span = OTEL_TRACER.spanBuilder(spanName).setSpanKind(SpanKind.SERVER).startSpan(); + Scope scope = span.makeCurrent(); + + Map spanDetails = createOTelSpanDetailsMap(span); + + scope.close(); + span.end(); + + return spanDetails; + } + + static Map createTransactionWhenServerSpanCreatedFromRemoteContext(String spanName) { + SpanContext remoteContext = SpanContext.createFromRemoteParent("da8bc8cc6d062849b0efcf3c169afb5a", "7d3efb1b173fecfa", TraceFlags.getSampled(), + TraceState.getDefault()); + + Span spanFromRemoteContext = OTEL_TRACER.spanBuilder(spanName).setSpanKind(SpanKind.SERVER) + .setParent(Context.current().with(Span.wrap(remoteContext))) + .startSpan(); + + Scope scope = spanFromRemoteContext.makeCurrent(); + + Map spanDetails = createOTelSpanDetailsMap(spanFromRemoteContext); + + scope.close(); + spanFromRemoteContext.end(); + + return spanDetails; + } + + @Trace(dispatcher = true) + static Map createTransactionWithServerSpanCreatedFromRemoteContext(String spanName) { + SpanContext remoteContext = SpanContext.createFromRemoteParent("da8bc8cc6d062849b0efcf3c169afb5a", "7d3efb1b173fecfa", TraceFlags.getSampled(), + TraceState.getDefault()); + + Span spanFromRemoteContext = OTEL_TRACER.spanBuilder(spanName).setSpanKind(SpanKind.SERVER) + .setParent(Context.current().with(Span.wrap(remoteContext))) + .startSpan(); + + Scope scope = spanFromRemoteContext.makeCurrent(); + + Map spanDetails = createOTelSpanDetailsMap(spanFromRemoteContext); + + scope.close(); + spanFromRemoteContext.end(); + + return spanDetails; + } + + static Map> doWorkInSpanWithInboundContext(String spanName, SpanKind spanKind) { + final TextMapPropagator propagator = W3CTraceContextPropagator.getInstance(); + + // mock out span with incoming w3c header propagation + TraceFlags traceFlags = TraceFlags.getDefault(); + TraceState traceState = TraceState.getDefault(); + SpanContext incomingSpanContext = SpanContext.create("da8bc8cc6d062849b0efcf3c169afb5a", "7d3efb1b173fecfa", traceFlags, traceState); + Span incomingSpan = Span.wrap(incomingSpanContext); + Context incomingContext = Context.current().with(incomingSpan); + Scope incomingScope = incomingSpan.makeCurrent(); + + Map carrier = new HashMap<>(); + TextMapSetter> setter = (carrier1, key, value) -> carrier1.put(key, value); + propagator.inject(incomingContext, carrier, setter); + + incomingScope.close(); + incomingSpan.end(); + + // extract inbound trace context and make it the current scope + Context extractedContext = propagator.extract(Context.current(), carrier, getter); + Scope extractedScope = extractedContext.makeCurrent(); + + // create a new span using the extracted context/scope + Span span = OTEL_TRACER.spanBuilder(spanName).setSpanKind(spanKind).startSpan(); + Context context = Context.current().with(span); + Scope scope = span.makeCurrent(); + + Map spanDetails = createOTelSpanDetailsMap(span); + + Map> mapOfMaps = new HashMap<>(); + mapOfMaps.put("carrier", carrier); + mapOfMaps.put("spanDetails", spanDetails); + + extractedScope.close(); + scope.close(); + span.end(); + + return mapOfMaps; + } + + private static final TextMapGetter> getter = new TextMapGetter>() { + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier.get(key); + } + }; + + @Trace + static Map emitOtelLogRecordNoTxn(Span span) { + emitOtelLogRecord(span); + return NewRelic.getAgent().getLinkingMetadata(); + } + + @Trace(dispatcher = true) + static Map emitOtelLogRecordInTxn(Span span) { + emitOtelLogRecord(span); + return NewRelic.getAgent().getLinkingMetadata(); + } + + static void emitOtelLogRecord(Span span) { + // create LogRecordBuilder + LogRecordBuilder logRecordBuilder = LOGGER.logRecordBuilder(); + + // build a LogRecord + Instant now = Instant.now(); + logRecordBuilder + .setBody("Generating OpenTelemetry LogRecord") + .setSeverity(Severity.ERROR) + .setSeverityText("OMG guise this is so severe!") + .setAttribute(AttributeKey.stringKey("foo"), "bar") + .setObservedTimestamp(now) + .setObservedTimestamp(now.toEpochMilli(), java.util.concurrent.TimeUnit.MILLISECONDS) + .setTimestamp(now) + .setTimestamp(now.toEpochMilli(), java.util.concurrent.TimeUnit.MILLISECONDS); + + if (span != null) { + logRecordBuilder.setContext(Context.current().with(span)); + } + + try { + throw new RuntimeException("This is a test exception for severity ERROR"); + } catch (RuntimeException e) { + logRecordBuilder.setAttribute(AttributeKey.stringKey("exception.message"), e.getMessage()); + logRecordBuilder.setAttribute(AttributeKey.stringKey("exception.type"), e.getClass().getName()); + logRecordBuilder.setAttribute(AttributeKey.stringKey("exception.stacktrace"), Arrays.toString(e.getStackTrace())); + } + + // emit the LogRecord + logRecordBuilder.emit(); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/context/SpanTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/context/SpanTest.java new file mode 100644 index 0000000000..c8daa371e2 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/context/SpanTest.java @@ -0,0 +1,300 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.context; + +import com.google.common.collect.ImmutableMap; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.SpanEvent; +import com.newrelic.agent.introspec.TracedMetricData; +import com.newrelic.agent.util.LatchingRunnable; +import com.newrelic.api.agent.Trace; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import com.nr.agent.instrumentation.utils.AttributesHelper; +import io.opentelemetry.sdk.trace.ExitTracerSpan; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +import static io.opentelemetry.sdk.trace.ExitTracerSpanTest.readSpanAttributes; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "io.opentelemetry" }, configName = "distributed_tracing.yml") +public class SpanTest { + static { + System.setProperty("otel.java.global-autoconfigure.enabled", "true"); + } + + static final Tracer OTEL_TRACER = GlobalOpenTelemetry.get().getTracer("test", "1.0"); + + @Test + public void testInternalSpansNoTransaction() { + Span span = OTEL_TRACER.spanBuilder("MyCustomSpan").startSpan(); + span.makeCurrent().close(); + span.end(); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + // no transactions because there was no dispatcher trace around the spans + assertEquals(0, introspector.getFinishedTransactionCount()); + introspector.clear(); + } + + @Test + public void testConsumerSpan() { + Span span = OTEL_TRACER.spanBuilder("consume").setSpanKind(SpanKind.CONSUMER).startSpan(); + span.makeCurrent().close(); + span.end(); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + final String txName = introspector.getTransactionNames().iterator().next(); + assertEquals("OtherTransaction/consume", txName); + + Map metricsForTransaction = InstrumentationTestRunner.getIntrospector().getMetricsForTransaction(txName); + + assertEquals(1, metricsForTransaction.size()); + assertTrue(metricsForTransaction.keySet().toString(), metricsForTransaction.containsKey("Span/consume")); + introspector.clear(); + } + + @Test + public void testServerSpan() throws IOException { + Map attributes = readSpanAttributes("server-span.json"); + final String spanName = (String) attributes.remove("name"); + + Span span = OTEL_TRACER.spanBuilder(spanName).setSpanKind(SpanKind.SERVER).startSpan(); + span.setAllAttributes(AttributesHelper.toAttributes(attributes)); + span.makeCurrent().close(); + span.end(); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + final String txName = introspector.getTransactionNames().iterator().next(); + assertEquals("WebTransaction/Uri/owners", txName); + + Map metricsForTransaction = InstrumentationTestRunner.getIntrospector().getMetricsForTransaction(txName); + + assertEquals(1, metricsForTransaction.size()); + assertTrue(metricsForTransaction.keySet().toString(), metricsForTransaction.containsKey("Span/GET /owners")); + introspector.clear(); + } + + @Test + public void testSimpleSpans() { + simpleSpans(); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + final String txName = introspector.getTransactionNames().iterator().next(); + assertEquals("OtherTransaction/Custom/io.opentelemetry.context.SpanTest/simpleSpans", txName); + + Map metricsForTransaction = InstrumentationTestRunner.getIntrospector().getMetricsForTransaction(txName); + + assertEquals(3, metricsForTransaction.size()); + assertTrue(metricsForTransaction.containsKey("Java/io.opentelemetry.context.SpanTest/simpleSpans")); + assertTrue(metricsForTransaction.containsKey("Span/MyCustomSpan")); + assertTrue(metricsForTransaction.containsKey("Span/kid")); + introspector.clear(); + } + + @Test + public void testAsyncSpans() { + final ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + asyncSpans(executor, SpanTest::asyncWork); + LatchingRunnable.drain(executor); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + final String txName = introspector.getTransactionNames().iterator().next(); + assertEquals("OtherTransaction/Custom/io.opentelemetry.context.SpanTest/asyncSpans", txName); + + Map metricsForTransaction = InstrumentationTestRunner.getIntrospector().getMetricsForTransaction(txName); + + assertEquals(3, metricsForTransaction.size()); + assertTrue(metricsForTransaction.containsKey("Java/io.opentelemetry.context.SpanTest/asyncSpans")); + assertTrue(metricsForTransaction.containsKey("Java/OpenTelemetry/AsyncScope")); + assertTrue(metricsForTransaction.containsKey("Span/MyCustomAsyncSpan")); + introspector.clear(); + } finally { + executor.shutdown(); + } + } + + @Test + public void testAsyncSpansWithParentNotWorking() { + final ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + asyncSpans(executor, context -> { + // this is the correct parent, but it's transaction has already finished, so + // we can't link it together + Span parent = Span.fromContext(context); + assertTrue(parent instanceof ExitTracerSpan); + + // however, we could use the trace id from the parent transaction. We'd have two metric + // transactions, but the distributed trace would display the relationship between the spans + Span span = OTEL_TRACER.spanBuilder("OrphanedSpan").setParent(context).startSpan(); + span.makeCurrent().close(); + span.end(); + }); + LatchingRunnable.drain(executor); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + // we have two transactions because the async activity isn't linked together + assertEquals(2, introspector.getFinishedTransactionCount()); + final String txName = "OtherTransaction/Custom/io.opentelemetry.context.SpanTest/asyncSpans"; + assertTrue(introspector.getTransactionNames().contains("OtherTransaction/Custom/io.opentelemetry.context.SpanTest/asyncSpans")); + assertTrue(introspector.getTransactionNames().contains("OtherTransaction/Custom")); + + Map metricsForTransaction = InstrumentationTestRunner.getIntrospector().getMetricsForTransaction(txName); + + assertEquals(1, metricsForTransaction.size()); + assertTrue(metricsForTransaction.containsKey("Java/io.opentelemetry.context.SpanTest/asyncSpans")); + introspector.clear(); + } finally { + executor.shutdown(); + } + } + + @Test + public void testDatabaseSpan() { + databaseSpan(); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + final String txName = introspector.getTransactionNames().iterator().next(); + assertEquals("OtherTransaction/Custom/io.opentelemetry.context.SpanTest/databaseSpan", txName); + + Map metricsForTransaction = InstrumentationTestRunner.getIntrospector().getMetricsForTransaction(txName); + + assertEquals(2, metricsForTransaction.size()); + assertTrue(metricsForTransaction.containsKey("Java/io.opentelemetry.context.SpanTest/databaseSpan")); + assertTrue(metricsForTransaction.containsKey("Datastore/statement/mysql/owners/select")); + + Collection spanEvents = introspector.getSpanEvents(); + assertEquals(2, spanEvents.size()); + SpanEvent dbSpan = spanEvents.stream() + .filter(span -> "datastore".equals(span.category())).findFirst().get(); + assertEquals("owners", dbSpan.getAgentAttributes().get("db.collection")); + assertEquals("SELECT * FROM owners WHERE ssn = ?", dbSpan.getAgentAttributes().get("db.statement")); + Arrays.asList("db.collection", "db.sql.table", "db.system", "db.operation").forEach(key -> { + assertNull(key, dbSpan.getUserAttributes().get(key)); + }); + introspector.clear(); + } + + @Trace(dispatcher = true) + static void databaseSpan() { + Span span = OTEL_TRACER.spanBuilder("owners select").setSpanKind(SpanKind.CLIENT) + .setAttribute("db.system", "mysql") + .setAttribute("db.operation", "select") + .setAttribute("db.sql.table", "owners") + .setAttribute("db.statement", "SELECT * FROM owners WHERE ssn = 4566661792") + .startSpan(); + span.end(); + } + + @Test + public void testExternalSpan() { + externalSpan(); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + final String txName = introspector.getTransactionNames().iterator().next(); + assertEquals("OtherTransaction/Custom/io.opentelemetry.context.SpanTest/externalSpan", txName); + + Map metricsForTransaction = InstrumentationTestRunner.getIntrospector().getMetricsForTransaction(txName); + + assertEquals(2, metricsForTransaction.size()); + assertTrue(metricsForTransaction.toString(), metricsForTransaction.containsKey("External/www.foo.bar/test/GET")); + assertTrue(metricsForTransaction.toString(), metricsForTransaction.containsKey("Java/io.opentelemetry.context.SpanTest/externalSpan")); + + Collection spanEvents = introspector.getSpanEvents(); + assertEquals(2, spanEvents.size()); + SpanEvent httpSpan = spanEvents.stream() + .filter(span -> "http".equals(span.category())).findFirst().get(); + Map agentAttributes = ImmutableMap.of( + "server.address", "www.foo.bar", + "server.port", 8080, + "http.url", "https://www.foo.bar:8080/search", + "peer.hostname", "www.foo.bar", + "http.method", "GET"); + assertEquals(agentAttributes.size(), httpSpan.getAgentAttributes().size()); + agentAttributes.forEach((key, value) -> assertEquals(value, httpSpan.getAgentAttributes().get(key))); + agentAttributes.forEach((key, value) -> assertNull(key, httpSpan.getUserAttributes().get(key))); + introspector.clear(); + } + + @Trace(dispatcher = true) + static void externalSpan() { + Span span = OTEL_TRACER.spanBuilder("example.com").setSpanKind(SpanKind.CLIENT) + .setAttribute("server.address", "www.foo.bar") + .setAttribute("url.full", "https://www.foo.bar:8080/search?q=OpenTelemetry#SemConv") + .setAttribute("server.port", 8080) + .setAttribute("http.request.method", "GET") + .startSpan(); + span.end(); + } + + @Trace(dispatcher = true) + static void simpleSpans() { + Span span = OTEL_TRACER.spanBuilder("MyCustomSpan").startSpan(); + Scope scope = span.makeCurrent(); + SpanContext spanContext = span.getSpanContext(); + assertNotNull(spanContext.getTraceId()); + assertNotNull(spanContext.getSpanId()); + assertSame(spanContext, span.getSpanContext()); + Span current = Span.current(); + assertEquals(span, current); + Span kid = OTEL_TRACER.spanBuilder("kid").setParent(Context.current()).startSpan(); + kid.end(); + scope.close(); + span.end(); + + withSpan(); + } + + @Trace(dispatcher = true) + static void asyncSpans(Executor executor, Consumer consumer) { + Context context = Context.current(); + executor.execute(Context.current().wrap(() -> consumer.accept(context))); + } + + static void asyncWork(Context context) { + Span span = OTEL_TRACER.spanBuilder("MyCustomAsyncSpan").startSpan(); + span.makeCurrent().close(); + span.end(); + } + + @WithSpan + static void withSpan() { + Span span = OTEL_TRACER.spanBuilder("kid").startSpan(); + span.end(); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySDKCustomizerTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySDKCustomizerTest.java new file mode 100644 index 0000000000..392bea9253 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySDKCustomizerTest.java @@ -0,0 +1,181 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.autoconfigure; + +import com.newrelic.api.agent.Agent; +import com.newrelic.api.agent.Config; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.NewRelic; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import junit.framework.TestCase; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_METRICS_EXCLUDE; +import static io.opentelemetry.sdk.autoconfigure.OpenTelemetrySDKCustomizer.SERVICE_INSTANCE_ID_ATTRIBUTE_KEY; +import static io.opentelemetry.sdk.metrics.data.AggregationTemporality.DELTA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OpenTelemetrySDKCustomizerTest extends TestCase { + + public void testApplyProperties() { + Agent agent = mock(Agent.class); + Logger logger = mock(Logger.class); + when(agent.getLogger()).thenReturn(logger); + Config config = mock(Config.class); + when(agent.getConfig()).thenReturn(config); + when(config.getValue("app_name")).thenReturn("Test"); + when(config.getValue("host")).thenReturn("mylaptop"); + when(config.getValue("license_key")).thenReturn("12345"); + + Map properties = OpenTelemetrySDKCustomizer.applyProperties(mock(ConfigProperties.class), agent); + assertEquals("api-key=12345", properties.get("otel.exporter.otlp.headers")); + assertEquals("https://mylaptop:443", properties.get("otel.exporter.otlp.endpoint")); + assertEquals("http/protobuf", properties.get("otel.exporter.otlp.protocol")); + assertEquals("Test", properties.get("otel.service.name")); + assertEquals("gzip", properties.get("otel.exporter.otlp.compression")); + } + + public void testApplyResourcesServiceInstanceIdSet() { + com.newrelic.agent.bridge.Agent agent = mock(com.newrelic.agent.bridge.Agent.class); + Resource resource = OpenTelemetrySDKCustomizer.applyResources( + Resource.create(Attributes.of(SERVICE_INSTANCE_ID_ATTRIBUTE_KEY, "7fjjr")), agent, mock(Logger.class)); + assertEquals("7fjjr", resource.getAttribute(SERVICE_INSTANCE_ID_ATTRIBUTE_KEY)); + assertNull(resource.getAttribute(AttributeKey.stringKey("entity.guid"))); + } + + public void testApplyResources() { + com.newrelic.agent.bridge.Agent agent = mock(com.newrelic.agent.bridge.Agent.class); + when(agent.getEntityGuid(true)).thenReturn("myguid"); + Resource resource = OpenTelemetrySDKCustomizer.applyResources( + Resource.empty(), agent, mock(Logger.class)); + assertNotNull(resource.getAttribute(SERVICE_INSTANCE_ID_ATTRIBUTE_KEY)); + assertEquals("myguid", resource.getAttribute(AttributeKey.stringKey("entity.guid"))); + } + + public void testApplyMeterExcludesDropsExcludedMeters() { + DummyExporter metricExporter = new DummyExporter(); + MetricReader reader = PeriodicMetricReader.create(metricExporter); + + SdkMeterProvider provider = setupProviderFromExcludesConfig(reader, "drop-me,drop-me-too,never-used"); + + //produce to some meters and force them to be exported + provider.get("drop-me").counterBuilder("foo").build().add(1); + provider.get("drop-me-too").counterBuilder("bar").build().add(2); + provider.get("keep-me").counterBuilder("baz").build().add(3); + provider.get("keep-me").counterBuilder("hello").build().add(3); + reader.forceFlush(); + + Collection collectedMetrics = metricExporter.getLatestMetricData(); + List collectedMetricNames = metricNames(collectedMetrics); + + assertEquals(2, collectedMetricNames.size()); + assertFalse(collectedMetricNames.contains("foo")); + assertFalse(collectedMetricNames.contains("bar")); + assertTrue(collectedMetricNames.contains("baz")); + assertTrue(collectedMetricNames.contains("hello")); + } + + public void testApplyMeterExcludesDropsNothingWhenEmpty() { + DummyExporter metricExporter = new DummyExporter(); + MetricReader reader = PeriodicMetricReader.create(metricExporter); + SdkMeterProvider provider = setupProviderFromExcludesConfig(reader, ""); + + //produce to some meters and force them to be exported + provider.get("keep-me").counterBuilder("foo").build().add(1); + provider.get("keep-me-too").counterBuilder("bar").build().add(3); + reader.forceFlush(); + + Collection collectedMetrics = metricExporter.getLatestMetricData(); + List actualMetricNames = metricNames(collectedMetrics); + + assertEquals(2, collectedMetrics.size()); + assertTrue(actualMetricNames.contains("foo")); + assertTrue(actualMetricNames.contains("bar")); + } + + private List metricNames(Collection collectedMetrics) { + List metricNames = new ArrayList<>(); + for (MetricData metricData : collectedMetrics) { + metricNames.add(metricData.getName()); + } + return metricNames; + } + + private SdkMeterProvider setupProviderFromExcludesConfig(MetricReader reader, String excludedMeters) { + Agent agent = mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Logger logger = mock(Logger.class); + when(agent.getLogger()).thenReturn(logger); + Mockito.when(agent.getConfig().getValue(OPENTELEMETRY_METRICS_EXCLUDE, "")).thenReturn(excludedMeters); + SdkMeterProviderBuilder customizedBuilder; + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(agent); + customizedBuilder = OpenTelemetrySDKCustomizer.applyMeterExcludes( + SdkMeterProvider.builder().registerMetricReader(reader), agent + ); + } + return customizedBuilder.build(); + } + + // A dummy exporter exposes the last round of collected metrics on its result field. + static class DummyExporter implements MetricExporter { + + //expose the most recently exported metrics + private Collection lastestMetricData = null; + + //this is required to register views without throwing an exception + @Override + public Aggregation getDefaultAggregation(InstrumentType instrumentType) { + return Aggregation.sum(); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return DELTA; + } + + @Override + public CompletableResultCode export(Collection metrics) { + this.lastestMetricData = metrics; + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return null; + } + + @Override + public CompletableResultCode shutdown() { + return null; + } + + public Collection getLatestMetricData() { + return lastestMetricData; + } + } +} \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/NRLogRecordTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/NRLogRecordTest.java new file mode 100644 index 0000000000..2e11a3df8d --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/NRLogRecordTest.java @@ -0,0 +1,137 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import com.nr.agent.instrumentation.utils.AttributesHelper; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.resources.Resource; +import junit.framework.TestCase; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class NRLogRecordTest extends TestCase { + static String exceptionMessage; + static String exceptionType; + static String exceptionStacktrace; + + public void testNRLogRecord() throws Exception { + final List emitted = new ArrayList<>(); + LogRecordProcessor logRecordProcessor = new LogRecordProcessor() { + @Override + public void onEmit(Context context, ReadWriteLogRecord logRecord) { + emitted.add(logRecord); + } + + @Override + public CompletableResultCode shutdown() { + return LogRecordProcessor.super.shutdown(); + } + + @Override + public CompletableResultCode forceFlush() { + return LogRecordProcessor.super.forceFlush(); + } + + @Override + public void close() { + LogRecordProcessor.super.close(); + } + }; + + final String instrumentationScopeName = "test"; + final String body = "This is a test log message"; + final String severityText = "This is severity text"; + + Logger logger = new TestLoggerBuilder(instrumentationScopeName).addLogRecordProcessor(logRecordProcessor) + .setResource(Resource.getDefault()) + .setSchemaUrl("https://opentelemetry.io/schemas/1.0.0") + .setInstrumentationVersion("1.0.0") + .build(); + + Map attributesMap = getAttributesMap(); + Attributes attributes = AttributesHelper.toAttributes(attributesMap); + + Instant now = Instant.now(); + logger.logRecordBuilder(). + setSeverity(Severity.INFO) + .setSeverityText(severityText) + .setBody(body) + .setAllAttributes(attributes) + .setAttribute(AttributeKey.stringKey("foo"), "bar") + .setObservedTimestamp(now) + .setTimestamp(now) + .setTimestamp(now.toEpochMilli(), java.util.concurrent.TimeUnit.MILLISECONDS) +// .setContext() + .emit(); + + assertEquals(1, emitted.size()); + + ReadWriteLogRecord readWriteLogRecord = emitted.get(0); + LogRecordData logRecordData = readWriteLogRecord.toLogRecordData(); + + assertEquals(instrumentationScopeName, logRecordData.getInstrumentationScopeInfo().getName()); + assertEquals("1.0.0", logRecordData.getInstrumentationScopeInfo().getVersion()); + + // The +1 is because one additional attribute is added via + // the explicit setAttribute(AttributeKey.stringKey("foo"), "bar") call. + assertEquals((attributesMap.size() + 1), logRecordData.getAttributes().size()); + assertEquals(4, logRecordData.getResource().getAttributes().size()); + assertEquals("opentelemetry", logRecordData.getResource().getAttributes().get(AttributeKey.stringKey("telemetry.sdk.name"))); + + assertEquals(exceptionMessage, logRecordData.getAttributes().get(AttributeKey.stringKey("exception.message"))); + assertEquals(exceptionType, logRecordData.getAttributes().get(AttributeKey.stringKey("exception.type"))); + assertEquals(exceptionStacktrace, logRecordData.getAttributes().get(AttributeKey.stringKey("exception.stacktrace"))); + + assertEquals("bar", logRecordData.getAttributes().get(AttributeKey.stringKey("foo"))); + + assertEquals(body, logRecordData.getBody().asString()); + assertEquals(Severity.INFO, logRecordData.getSeverity()); + assertEquals(severityText, logRecordData.getSeverityText()); + + long observedTimestampEpochNanos = TimeUnit.SECONDS.toNanos(now.getEpochSecond()) + now.getNano(); + assertEquals(observedTimestampEpochNanos, logRecordData.getObservedTimestampEpochNanos()); + + final long threadId = Thread.currentThread().getId(); + final String threadName = Thread.currentThread().getName(); + + assertEquals(threadId, ((NRLogRecord.BasicLogRecordData) logRecordData).getThreadId()); + assertEquals(threadName, ((NRLogRecord.BasicLogRecordData) logRecordData).getThreadName()); + } + + private static Map getAttributesMap() { + Map attributesMap = new HashMap() {{ + put("service.name", "test-service"); + put("service.version", "1.0.0"); + put("environment", "production"); + }}; + + try { + throw new RuntimeException("This is a test exception for severity ERROR"); + } catch (RuntimeException e) { + exceptionMessage = e.getMessage(); + exceptionType = e.getClass().toString(); + exceptionStacktrace = Arrays.toString(e.getStackTrace()); + attributesMap.put("exception.message", exceptionMessage); + attributesMap.put("exception.type", exceptionType); + attributesMap.put("exception.stacktrace", exceptionStacktrace); + } + return attributesMap; + } +} \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/NRLoggerBuilderTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/NRLoggerBuilderTest.java new file mode 100644 index 0000000000..0787098a2a --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/NRLoggerBuilderTest.java @@ -0,0 +1,25 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.resources.Resource; +import junit.framework.TestCase; + +public class NRLoggerBuilderTest extends TestCase { + final LoggerSharedState LOGGER_SHARED_STATE = new LoggerSharedState(Resource.empty(), LogLimits::getDefault, NoopLogRecordProcessor.getInstance(), + Clock.getDefault()); + + public void testBuild() { + Logger logger = new NRLoggerBuilder("test-lib", LOGGER_SHARED_STATE).build(); + + assertTrue(logger.getClass().getName(), logger.getClass().getName().startsWith( + "io.opentelemetry.sdk.logs.NRLoggerBuilder")); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/TestLoggerBuilder.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/TestLoggerBuilder.java new file mode 100644 index 0000000000..b8358ef97d --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/TestLoggerBuilder.java @@ -0,0 +1,56 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.LoggerBuilder; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.resources.Resource; + +import java.util.function.Supplier; + +public class TestLoggerBuilder implements LoggerBuilder { + private final String instrumentationScopeName; + private String instrumentationScopeVersion; + private LogRecordProcessor logRecordProcessor; + private Resource resource = Resource.empty(); + private String schemaUrl; + + public TestLoggerBuilder(String instrumentationScopeName) { + this.instrumentationScopeName = instrumentationScopeName; + } + + public TestLoggerBuilder addLogRecordProcessor(LogRecordProcessor processor) { + this.logRecordProcessor = processor; + return this; + } + + public TestLoggerBuilder setResource(Resource resource) { + this.resource = resource; + return this; + } + + @Override + public LoggerBuilder setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + @Override + public LoggerBuilder setInstrumentationVersion(String instrumentationScopeVersion) { + this.instrumentationScopeVersion = instrumentationScopeVersion; + return this; + } + + @Override + public Logger build() { + Supplier logLimitsSupplier = () -> LogLimits.getDefault(); + LoggerSharedState sharedState = new LoggerSharedState(resource, logLimitsSupplier, logRecordProcessor, Clock.getDefault()); + return () -> new NRLogRecordBuilder(instrumentationScopeName, instrumentationScopeVersion, schemaUrl, sharedState); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/ExitTracerSpanTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/ExitTracerSpanTest.java new file mode 100644 index 0000000000..bb3cbcd255 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/ExitTracerSpanTest.java @@ -0,0 +1,159 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.agent.bridge.ExitTracer; +import com.newrelic.agent.security.deps.com.fasterxml.jackson.databind.ObjectMapper; +import com.newrelic.api.agent.DatastoreParameters; +import com.newrelic.api.agent.ExternalParameters; +import com.newrelic.api.agent.HttpParameters; +import com.nr.agent.instrumentation.utils.AttributesHelper; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.InstrumentationLibraryInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.SpanData; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class ExitTracerSpanTest { + private static final Consumer END_HANDLER = span -> {}; + + @Test + public void testReportDatabaseClientSpan() throws Exception { + final Map attributes = readSpanAttributes("db-span.json"); + + ExitTracer tracer = mock(ExitTracer.class); + final List started = new ArrayList<>(); + final List ended = new ArrayList<>(); + SpanProcessor processor = new SpanProcessor() { + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + started.add(span); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) { + ended.add(span); + } + + @Override + public boolean isEndRequired() { + return true; + } + }; + SpanBuilder spanBuilder = new TestTracerBuilder("test").addSpanProcessor(processor) + .setResource(Resource.getDefault()) + .withTracer(tracer).build().spanBuilder((String) attributes.remove("name")); + spanBuilder.setSpanKind(SpanKind.CLIENT).setAllAttributes(AttributesHelper.toAttributes(attributes)).startSpan().end(); + + assertEquals(1, started.size()); + assertEquals(1, ended.size()); + ReadWriteSpan startedSpan = started.get(0); + assertEquals("SELECT petclinic", startedSpan.getName()); + SpanData spanData = startedSpan.toSpanData(); + assertEquals(49, spanData.getAttributes().size()); + assertEquals(4, spanData.getResource().getAttributes().size()); + assertEquals("opentelemetry", spanData.getResource().getAttributes().get(AttributeKey.stringKey("telemetry.sdk.name"))); + + //new ExitTracerSpan(tracer, SpanKind.CLIENT, attributes, END_HANDLER).end(); + final ArgumentCaptor dbParams = ArgumentCaptor.forClass(DatastoreParameters.class); + verify(tracer, times(1)).reportAsExternal(dbParams.capture()); + assertEquals("mysql", dbParams.getValue().getProduct()); + assertEquals("owners", dbParams.getValue().getCollection()); + assertEquals("mysqlserver", dbParams.getValue().getHost()); + assertEquals(3306, dbParams.getValue().getPort().intValue()); + assertEquals("SELECT", dbParams.getValue().getOperation()); + assertEquals("petclinic", dbParams.getValue().getDatabaseName()); + } + + @Test + public void testReportDatabaseClientSpanMissingSqlTable() throws Exception { + ExitTracer tracer = mock(ExitTracer.class); + Map attributes = readSpanAttributes("db-span.json"); + attributes.remove("db.sql.table"); + new ExitTracerSpan(tracer, InstrumentationLibraryInfo.empty(), SpanKind.CLIENT, "", SpanContext.getInvalid(), Resource.empty(),attributes, END_HANDLER).end(); + final ArgumentCaptor dbParams = ArgumentCaptor.forClass(DatastoreParameters.class); + verify(tracer, times(1)).reportAsExternal(dbParams.capture()); + assertEquals("mysql", dbParams.getValue().getProduct()); + assertNull(dbParams.getValue().getCollection()); + assertEquals("mysqlserver", dbParams.getValue().getHost()); + assertEquals(3306, dbParams.getValue().getPort().intValue()); + assertEquals("SELECT", dbParams.getValue().getOperation()); + assertEquals("petclinic", dbParams.getValue().getDatabaseName()); + } + + @Test + public void testReportRpcClientSpan() throws Exception { + ExitTracer tracer = mock(ExitTracer.class); + new ExitTracerSpan(tracer, InstrumentationLibraryInfo.empty(), SpanKind.CLIENT, "", SpanContext.getInvalid(), Resource.empty(), readSpanAttributes("external-rpc-span.json"), END_HANDLER).end(); + final ArgumentCaptor externalParams = ArgumentCaptor.forClass(HttpParameters.class); + verify(tracer, times(1)).reportAsExternal(externalParams.capture()); + assertEquals("io.opentelemetry.grpc-1.6", externalParams.getValue().getLibrary()); + assertEquals("ResolveBoolean", externalParams.getValue().getProcedure()); + assertEquals("http://opentelemetry-demo-flagd:8013", externalParams.getValue().getUri().toString()); + } + + @Test + public void testReportHttpClientSpan() throws Exception { + ExitTracer tracer = mock(ExitTracer.class); + new ExitTracerSpan(tracer, InstrumentationLibraryInfo.empty(), SpanKind.CLIENT, "", SpanContext.getInvalid(), Resource.empty(),readSpanAttributes("external-http-span.json"), END_HANDLER).end(); + final ArgumentCaptor externalParams = ArgumentCaptor.forClass(HttpParameters.class); + verify(tracer, times(1)).reportAsExternal(externalParams.capture()); + assertEquals("io.opentelemetry.java-http-client", externalParams.getValue().getLibrary()); + assertEquals("https://google.com", externalParams.getValue().getUri().toString()); + assertEquals("GET", externalParams.getValue().getProcedure()); + } + + @Test + public void testReportHttpClientSpanWithCodeFunction() throws Exception { + ExitTracer tracer = mock(ExitTracer.class); + new ExitTracerSpan(tracer, InstrumentationLibraryInfo.empty(), SpanKind.CLIENT, "", SpanContext.getInvalid(), Resource.empty(),readSpanAttributes("external-http-span.json"), END_HANDLER) + .setAttribute(AttributeKey.stringKey("code.function"), "execute").end(); + final ArgumentCaptor externalParams = ArgumentCaptor.forClass(HttpParameters.class); + verify(tracer, times(1)).reportAsExternal(externalParams.capture()); + assertEquals("io.opentelemetry.java-http-client", externalParams.getValue().getLibrary()); + assertEquals("https://google.com", externalParams.getValue().getUri().toString()); + assertEquals("execute", externalParams.getValue().getProcedure()); + } + + @Test + public void testBadClientSpan() throws Exception { + ExitTracer tracer = mock(ExitTracer.class); + new ExitTracerSpan(tracer, InstrumentationLibraryInfo.empty(), SpanKind.CLIENT, "", SpanContext.getInvalid(), Resource.empty(),readSpanAttributes("bad-client-span.json"), END_HANDLER).end(); + verify(tracer, times(0)).reportAsExternal(any(ExternalParameters.class)); + } + + public static Map readSpanAttributes(String fileName) throws IOException { + try (InputStream in = ExitTracerSpanTest.class.getResourceAsStream(fileName)) { + return new ObjectMapper().readValue(in, Map.class); + } + } +} \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/NRTracerBuilderTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/NRTracerBuilderTest.java new file mode 100644 index 0000000000..22eca6c24c --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/NRTracerBuilderTest.java @@ -0,0 +1,49 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.api.agent.Agent; +import com.newrelic.api.agent.NewRelic; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import junit.framework.TestCase; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Collections; + +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_TRACES_EXCLUDE; +import static com.nr.agent.instrumentation.utils.config.OpenTelemetryConfig.OPENTELEMETRY_TRACES_INCLUDE; + +public class NRTracerBuilderTest extends TestCase { + final TracerSharedState TRACER_SHARED_STATE = new TracerSharedState(Clock.getDefault(), IdGenerator.random(), + Resource.empty(), SpanLimits::getDefault, Sampler.alwaysOn(), Collections.emptyList()); + + public void testBuild() { + Tracer tracer = new NRTracerBuilder("test-lib", + TRACER_SHARED_STATE).build(); + assertTrue(tracer.getClass().getName(), tracer.getClass().getName().startsWith( + "io.opentelemetry.sdk.trace.NRTracerBuilder")); + } + + public void testBuildDisabled() { + Agent mockAgent = Mockito.mock(Agent.class, Mockito.RETURNS_DEEP_STUBS); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_EXCLUDE, "")).thenReturn("test-lib"); + Mockito.when(mockAgent.getConfig().getValue(OPENTELEMETRY_TRACES_INCLUDE, "")).thenReturn(""); + + try (MockedStatic mockNewRelic = Mockito.mockStatic(NewRelic.class)) { + mockNewRelic.when(NewRelic::getAgent).thenReturn(mockAgent); + Tracer tracer = new NRTracerBuilder("test-lib", + TRACER_SHARED_STATE).build(); + assertSame(OpenTelemetry.noop().getTracer("dude"), tracer); + } + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/TestTracerBuilder.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/TestTracerBuilder.java new file mode 100644 index 0000000000..a432c1d0b0 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/TestTracerBuilder.java @@ -0,0 +1,70 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.agent.bridge.ExitTracer; +import com.newrelic.agent.bridge.Instrumentation; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerBuilder; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.samplers.Sampler; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TestTracerBuilder implements TracerBuilder { + private final String instrumentationScopeName; + private String instrumentationScopeVersion; + private final Instrumentation instrumentation = mock(Instrumentation.class); + private final List spanProcessors = new ArrayList<>(); + private Resource resource = Resource.empty(); + + public TestTracerBuilder(String instrumentationScopeName) { + this.instrumentationScopeName = instrumentationScopeName; + } + + public TestTracerBuilder addSpanProcessor(SpanProcessor processor) { + this.spanProcessors.add(processor); + return this; + } + + public TestTracerBuilder setResource(Resource resource) { + this.resource = resource; + return this; + } + + @Override + public TracerBuilder setSchemaUrl(String schemaUrl) { + return this; + } + + @Override + public TracerBuilder setInstrumentationVersion(String instrumentationScopeVersion) { + return this; + } + + public TracerBuilder withTracer(ExitTracer tracer) { + when(instrumentation.createTracer(anyString(), anyInt())).thenReturn(tracer); + return this; + } + + @Override + public Tracer build() { + Supplier spanLimitsSupplier = () -> SpanLimits.getDefault(); + TracerSharedState sharedState = new TracerSharedState(Clock.getDefault(), IdGenerator.random(), resource, spanLimitsSupplier, Sampler.alwaysOn(), + spanProcessors); + return spanName -> new NRSpanBuilder(instrumentation, instrumentationScopeName, instrumentationScopeVersion, sharedState, spanName); + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/test/config/util/SaveSystemPropertyProviderRule.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/test/config/util/SaveSystemPropertyProviderRule.java new file mode 100644 index 0000000000..e17782fad4 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/test/config/util/SaveSystemPropertyProviderRule.java @@ -0,0 +1,94 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package test.config.util; + +import com.newrelic.agent.config.EnvironmentFacade; +import com.newrelic.agent.config.SystemPropertyFactory; +import com.newrelic.agent.config.SystemPropertyProvider; +import com.newrelic.agent.config.SystemProps; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.Collections; +import java.util.Map; +import java.util.Properties; + +public class SaveSystemPropertyProviderRule implements TestRule { + public void mockSingleProperty(String key, boolean value) { + mockSingleProperty(key, String.valueOf(value)); + } + + public void mockSingleProperty(String key, String propertyValue) { + Properties properties = new Properties(); + properties.put(key, propertyValue); + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new TestSystemProps(properties), + new TestEnvironmentFacade() + )); + } + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + SystemPropertyProvider previousProvider = SystemPropertyFactory.getSystemPropertyProvider(); + try { + base.evaluate(); + } finally { + SystemPropertyFactory.setSystemPropertyProvider(previousProvider); + } + } + }; + } + + public static class TestSystemProps extends SystemProps { + private final Properties systemProperties; + + public TestSystemProps() { + this(new Properties()); + } + + public TestSystemProps(Properties sys) { + systemProperties = sys; + } + + @Override + public String getSystemProperty(String prop) { + return systemProperties.getProperty(prop); + } + + @Override + public Properties getAllSystemProperties() { + return systemProperties; + } + } + + public static class TestEnvironmentFacade extends EnvironmentFacade { + private final Map envProperties; + + public TestEnvironmentFacade() { + this(Collections.emptyMap()); + } + + public TestEnvironmentFacade(Map envProperties) { + this.envProperties = envProperties; + } + + @Override + public String getenv(String key) { + return envProperties.get(key); + } + + @Override + public Map getAllEnvProperties() { + return envProperties; + } + } +} diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/attribute-mappings.json b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/attribute-mappings.json new file mode 100644 index 0000000000..1a27f6d41f --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/attribute-mappings.json @@ -0,0 +1,157 @@ +[ + { + "spanKind": "SERVER", + "attributeTypes": [ + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "HTTP-Server:1.23,HTTP-Server:1.24,HTTP-Server:1.25" + }, + { + "name": "net.host.port", + "version": "HTTP-Server:1.20" + } + ] + }, + { + "attributeType": "Host", + "attributes": [ + { + "name": "server.address", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.name", + "version": "HTTP-Server:1.20" + } + ] + } + ] + }, + { + "spanKind": "INTERNAL", + "attributeTypes": [ + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "" + }, + { + "name": "net.host.port", + "version": "HTTP-Server:1.20" + } + ] + }, + { + "attributeType": "Host", + "attributes": [ + { + "name": "server.address", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.name", + "version": "HTTP-Server:1.20" + } + ] + } + ] + }, + { + "spanKind": "CONSUMER", + "attributeTypes": [ + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.port", + "version": "HTTP-Server:1.20" + } + ] + }, + { + "attributeType": "Host", + "attributes": [ + { + "name": "server.address", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.name", + "version": "HTTP-Server:1.20" + } + ] + } + ] + }, + { + "spanKind": "PRODUCER", + "attributeTypes": [ + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.port", + "version": "HTTP-Server:1.20" + } + ] + }, + { + "attributeType": "Host", + "attributes": [ + { + "name": "server.address", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.name", + "version": "HTTP-Server:1.20" + } + ] + } + ] + }, + { + "spanKind": "CLIENT", + "attributeTypes": [ + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.port", + "version": "HTTP-Server:1.20" + } + ] + }, + { + "attributeType": "Host", + "attributes": [ + { + "name": "server.address", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.name", + "version": "HTTP-Server:1.20" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/distributed_tracing.yml b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/distributed_tracing.yml new file mode 100644 index 0000000000..bb798f85d5 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/distributed_tracing.yml @@ -0,0 +1,20 @@ +common: &default_settings + distributed_tracing: + enabled: true + + opentelemetry: + enabled: true + # this was previously used to enable/disable metrics +# sdk: +# autoconfigure: +# enabled: false + logs: + enabled: true + metrics: + enabled: true + #include: + #exclude: + traces: + enabled: true + #include: + #exclude: diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/bad-client-span.json b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/bad-client-span.json new file mode 100644 index 0000000000..fa79d74b83 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/bad-client-span.json @@ -0,0 +1,56 @@ +{ + "cloud.account.id": "opentracing-265818", + "cloud.availability_zone": "us-central1-a", + "cloud.platform": "gcp_kubernetes_engine", + "cloud.provider": "gcp", + "container.id": "237ba1f2fbce91b54da664202ed8c3154c3ed65992eb08823bdae8a173718404", + "duration.ms": 1.881907, + "entity.guid": "MTA5Mzk5NzR8RVhUfFNFUlZJQ0V8MTE5NzIxNzM2MDk3MzYyODQ0MA", + "entity.name": "adservice", + "entity.type": "SERVICE", + "entityGuid": "MTA5Mzk5NzR8RVhUfFNFUlZJQ0V8MTE5NzIxNzM2MDk3MzYyODQ0MA", + "feature_flag.key": "adServiceFailure", + "feature_flag.provider_name": "flagd", + "host.arch": "amd64", + "host.id": "gcp-243707756122848312", + "host.name": "gke-teddy-opentelemetry--default-pool-22bb6e2e-dpc6", + "id": "68599eb449c78da5", + "instrumentation.provider": "opentelemetry", + "k8s.cluster.name": "teddy-opentelemetry-demo", + "k8s.deployment.name": "opentelemetry-demo-adservice", + "k8s.namespace.name": "otel-demo", + "k8s.node.name": "gke-teddy-opentelemetry--default-pool-22bb6e2e-dpc6", + "k8s.pod.ip": "10.104.1.6", + "k8s.pod.name": "opentelemetry-demo-adservice-8b4f49c74-vh7r2", + "k8s.pod.start_time": "2024-04-11T15:44:13Z", + "k8s.pod.uid": "ea4cc312-dc6f-43fb-bf42-380eb94dde64", + "name": "resolve", + "newRelic.ingestPoint": "api.traces.otlp", + "newrelic.source": "api.traces.otlp", + "nr.invalidAttributeCount": 1, + "os.description": "Linux 5.15.146+", + "os.type": "linux", + "otel.library.name": "OpenFeature/dev.openfeature.contrib.providers.flagd", + "otel.library.version": "", + "parent.id": "df0e1f82cd58105d", + "process.command_line": "/opt/java/openjdk/bin/java -javaagent:/usr/src/app/opentelemetry-javaagent.jar oteldemo.AdService", + "process.executable.path": "/opt/java/openjdk/bin/java", + "process.id": "df0e1f82cd58105d", + "process.pid": 1, + "process.runtime.description": "Eclipse Adoptium OpenJDK 64-Bit Server VM 21.0.2+13-LTS", + "process.runtime.name": "OpenJDK Runtime Environment", + "process.runtime.version": "21.0.2+13-LTS", + "service.instance.id": "ea4cc312-dc6f-43fb-bf42-380eb94dde64", + "service.name": "adservice", + "service.namespace": "opentelemetry-demo", + "span.kind": "client", + "telemetry.distro.name": "opentelemetry-java-instrumentation", + "telemetry.distro.version": "2.0.0", + "telemetry.sdk.language": "java", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.34.1", + "thread.id": 1386, + "thread.name": "grpc-default-executor-670", + "timestamp": 1714497468129, + "trace.id": "30f08e412c7d612577b7b8d617879698" +} \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/db-span.json b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/db-span.json new file mode 100644 index 0000000000..394e1d3533 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/db-span.json @@ -0,0 +1,51 @@ +{ + "category": "datastore", + "container.id": "c4fe17ff1aa22c9e00df7885bdaa2f2d856ec7c528537b1f265b843f05c78010", + "db.connection_string": "mysql://mysqlserver:3306", + "db.name": "petclinic", + "db.operation": "SELECT", + "db.sql.table": "owners", + "db.statement": "select count(distinct o1_0.id) from owners o1_0 left join pets p1_0 on o1_0.id=p1_0.owner_id where o1_0.last_name like ? escape ?", + "db.system": "mysql", + "db.user": "petclinic", + "duration.ms": 0.475583, + "entity.guid": "MTEzMTk5MDd8RVhUfFNFUlZJQ0V8LTExODgyMzk4OTM0MTY1Nzk1MjQ", + "entity.name": "PetClinic-saxon", + "entity.type": "SERVICE", + "entityGuid": "MTEzMTk5MDd8RVhUfFNFUlZJQ0V8LTExODgyMzk4OTM0MTY1Nzk1MjQ", + "host.arch": "aarch64", + "host.name": "c4fe17ff1aa2", + "id": "8ef69c1ad32c0b17", + "instrumentation.provider": "opentelemetry", + "name": "SELECT petclinic", + "newRelic.ingestPoint": "api.traces.otlp", + "newrelic.source": "api.traces.otlp", + "nr.categories": ":datastore:", + "nr.invalidAttributeCount": 1, + "os.description": "Linux 6.5.11-linuxkit", + "os.type": "linux", + "otel.library.name": "io.opentelemetry.jdbc", + "otel.library.version": "1.33.0-alpha", + "parent.id": "b47b915d6cc4d819", + "process.command_args": "", + "process.executable.path": "/opt/java/openjdk/bin/java", + "process.id": "59370658d7b61ec9", + "process.pid": 1, + "process.runtime.description": "Eclipse Adoptium OpenJDK 64-Bit Server VM 17.0.10+7", + "process.runtime.name": "OpenJDK Runtime Environment", + "process.runtime.version": "17.0.10+7", + "server.address": "mysqlserver", + "server.port": 3306, + "service.instance.id": "itAAmTeOuJ7pFVo", + "service.name": "PetClinic-saxon", + "service.version": "3.2.0-SNAPSHOT", + "span.kind": "client", + "telemetry.auto.version": "1.33.0", + "telemetry.sdk.language": "java", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.35.0", + "thread.id": 45, + "thread.name": "http-nio-8080-exec-5", + "timestamp": 1714427973075, + "trace.id": "07c5d2f8972e783b33f5b6fa57308638" +} \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/external-http-span.json b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/external-http-span.json new file mode 100644 index 0000000000..e312ae9177 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/external-http-span.json @@ -0,0 +1,47 @@ +{ + "category": "http", + "container.id": "c4fe17ff1aa22c9e00df7885bdaa2f2d856ec7c528537b1f265b843f05c78010", + "duration.ms": 304.125292, + "entity.guid": "MTEzMTk5MDd8RVhUfFNFUlZJQ0V8LTExODgyMzk4OTM0MTY1Nzk1MjQ", + "entity.name": "PetClinic-saxon", + "entity.type": "SERVICE", + "entityGuid": "MTEzMTk5MDd8RVhUfFNFUlZJQ0V8LTExODgyMzk4OTM0MTY1Nzk1MjQ", + "host.arch": "aarch64", + "host.name": "c4fe17ff1aa2", + "http.request.method": "GET", + "http.response.status_code": 301, + "id": "5aeb8ccf27d29d66", + "instrumentation.provider": "opentelemetry", + "name": "GET", + "network.protocol.version": "2", + "newRelic.ingestPoint": "api.traces.otlp", + "newrelic.source": "api.traces.otlp", + "nr.categories": ":http:", + "nr.invalidAttributeCount": 1, + "os.description": "Linux 6.5.11-linuxkit", + "os.type": "linux", + "otel.library.name": "io.opentelemetry.java-http-client", + "otel.library.version": "1.33.0-alpha", + "parent.id": "935b92e59644b6bf", + "process.command_args": "", + "process.executable.path": "/opt/java/openjdk/bin/java", + "process.id": "55f2d1f8c6d40e1f", + "process.pid": 1, + "process.runtime.description": "Eclipse Adoptium OpenJDK 64-Bit Server VM 17.0.10+7", + "process.runtime.name": "OpenJDK Runtime Environment", + "process.runtime.version": "17.0.10+7", + "server.address": "google.com", + "service.instance.id": "itAAmTeOuJ7pFVo", + "service.name": "PetClinic-saxon", + "service.version": "3.2.0-SNAPSHOT", + "span.kind": "client", + "telemetry.auto.version": "1.33.0", + "telemetry.sdk.language": "java", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.35.0", + "thread.id": 44, + "thread.name": "http-nio-8080-exec-4", + "timestamp": 1714428373323, + "trace.id": "8ce187a446c21ab4159a5a3ece7e54ad", + "url.full": "https://google.com" +} \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/external-rpc-span.json b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/external-rpc-span.json new file mode 100644 index 0000000000..08afe7c189 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/external-rpc-span.json @@ -0,0 +1,64 @@ +{ + "cloud.account.id": "opentracing-265818", + "cloud.availability_zone": "us-central1-a", + "cloud.platform": "gcp_kubernetes_engine", + "cloud.provider": "gcp", + "container.id": "237ba1f2fbce91b54da664202ed8c3154c3ed65992eb08823bdae8a173718404", + "duration.ms": 1.731308, + "entity.guid": "MTA5Mzk5NzR8RVhUfFNFUlZJQ0V8MTE5NzIxNzM2MDk3MzYyODQ0MA", + "entity.name": "adservice", + "entity.type": "SERVICE", + "entityGuid": "MTA5Mzk5NzR8RVhUfFNFUlZJQ0V8MTE5NzIxNzM2MDk3MzYyODQ0MA", + "host.arch": "amd64", + "host.id": "gcp-243707756122848312", + "host.name": "gke-teddy-opentelemetry--default-pool-22bb6e2e-dpc6", + "id": "977ab2fb6909a327", + "instrumentation.provider": "opentelemetry", + "k8s.cluster.name": "teddy-opentelemetry-demo", + "k8s.deployment.name": "opentelemetry-demo-adservice", + "k8s.namespace.name": "otel-demo", + "k8s.node.name": "gke-teddy-opentelemetry--default-pool-22bb6e2e-dpc6", + "k8s.pod.ip": "10.104.1.6", + "k8s.pod.name": "opentelemetry-demo-adservice-8b4f49c74-vh7r2", + "k8s.pod.start_time": "2024-04-11T15:44:13Z", + "k8s.pod.uid": "ea4cc312-dc6f-43fb-bf42-380eb94dde64", + "name": "flagd.evaluation.v1.Service/ResolveBoolean", + "network.peer.address": "10.121.67.191", + "network.peer.port": 8013, + "network.type": "ipv4", + "newRelic.ingestPoint": "api.traces.otlp", + "newrelic.source": "api.traces.otlp", + "nr.invalidAttributeCount": 1, + "nr.spanEventCount": 2, + "os.description": "Linux 5.15.146+", + "os.type": "linux", + "otel.library.name": "io.opentelemetry.grpc-1.6", + "otel.library.version": "2.0.0-alpha", + "parent.id": "41c73413b4146f8a", + "process.command_line": "/opt/java/openjdk/bin/java -javaagent:/usr/src/app/opentelemetry-javaagent.jar oteldemo.AdService", + "process.executable.path": "/opt/java/openjdk/bin/java", + "process.id": "a46c5733c4028806", + "process.pid": 1, + "process.runtime.description": "Eclipse Adoptium OpenJDK 64-Bit Server VM 21.0.2+13-LTS", + "process.runtime.name": "OpenJDK Runtime Environment", + "process.runtime.version": "21.0.2+13-LTS", + "rpc.grpc.status_code": 0, + "rpc.method": "ResolveBoolean", + "rpc.service": "flagd.evaluation.v1.Service", + "rpc.system": "grpc", + "server.address": "opentelemetry-demo-flagd", + "server.port": 8013, + "service.instance.id": "ea4cc312-dc6f-43fb-bf42-380eb94dde64", + "service.name": "adservice", + "service.namespace": "opentelemetry-demo", + "span.kind": "client", + "telemetry.distro.name": "opentelemetry-java-instrumentation", + "telemetry.distro.version": "2.0.0", + "telemetry.sdk.language": "java", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.34.1", + "thread.id": 1322, + "thread.name": "grpc-default-executor-632", + "timestamp": 1714415451412, + "trace.id": "65a744b6cd40e6b98152b853d9dfab2b" +} \ No newline at end of file diff --git a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/server-span.json b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/server-span.json new file mode 100644 index 0000000000..592ff245fb --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/io/opentelemetry/sdk/trace/server-span.json @@ -0,0 +1,54 @@ +{ + "category": "http", + "container.id": "c4fe17ff1aa22c9e00df7885bdaa2f2d856ec7c528537b1f265b843f05c78010", + "duration.ms": 9.47825, + "entity.guid": "MTEzMTk5MDd8RVhUfFNFUlZJQ0V8LTExODgyMzk4OTM0MTY1Nzk1MjQ", + "entity.name": "PetClinic-saxon", + "entity.type": "SERVICE", + "entityGuid": "MTEzMTk5MDd8RVhUfFNFUlZJQ0V8LTExODgyMzk4OTM0MTY1Nzk1MjQ", + "host.arch": "aarch64", + "host.name": "c4fe17ff1aa2", + "http.request.method": "GET", + "http.response.status_code": 200, + "http.route": "/owners", + "id": "03aca13482b8ebac", + "instrumentation.provider": "opentelemetry", + "name": "GET /owners", + "network.peer.address": "192.168.65.1", + "network.peer.port": 50130, + "network.protocol.version": "1.1", + "newRelic.ingestPoint": "api.traces.otlp", + "newrelic.source": "api.traces.otlp", + "nr.categories": ":http:", + "nr.invalidAttributeCount": 1, + "os.description": "Linux 6.5.11-linuxkit", + "os.type": "linux", + "otel.library.name": "io.opentelemetry.tomcat-10.0", + "otel.library.version": "1.33.0-alpha", + "process.command_args": "", + "process.executable.path": "/opt/java/openjdk/bin/java", + "process.id": "03aca13482b8ebac", + "process.pid": 1, + "process.runtime.description": "Eclipse Adoptium OpenJDK 64-Bit Server VM 17.0.10+7", + "process.runtime.name": "OpenJDK Runtime Environment", + "process.runtime.version": "17.0.10+7", + "server.address": "localhost", + "server.port": 8082, + "service.instance.id": "itAAmTeOuJ7pFVo", + "service.name": "PetClinic-saxon", + "service.version": "3.2.0-SNAPSHOT", + "span.kind": "server", + "telemetry.auto.version": "1.33.0", + "telemetry.sdk.language": "java", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": "1.35.0", + "thread.id": 47, + "thread.name": "http-nio-8080-exec-7", + "timestamp": 1714429208409, + "trace.id": "940efa58589ecce9122c79bcb00e2f16", + "transaction.name": "WebTransaction/server/GET /owners", + "url.path": "/owners", + "url.query": "lastName=", + "url.scheme": "http", + "user_agent.original": "Apache-HttpClient/4.5.14 (Java/17.0.8.1)" +} \ No newline at end of file diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java b/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java index 806889b3ce..03960c7813 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java @@ -128,7 +128,11 @@ public class Transaction { static final ClassMethodSignature SCALA_API_TXN_CLASS_SIGNATURE = new ClassMethodSignature( "newrelic.scala.api.TraceOps$", "txn", null); public static final int SCALA_API_TXN_CLASS_SIGNATURE_ID = - ClassMethodSignatures.get().add(SCALA_API_TXN_CLASS_SIGNATURE); + ClassMethodSignatures.get().add(SCALA_API_TXN_CLASS_SIGNATURE); + + public static final int GENERIC_TXN_CLASS_SIGNATURE_ID = + ClassMethodSignatures.get().add(new ClassMethodSignature("", "", null)); + private static final String THREAD_ASSERTION_FAILURE = "Thread assertion failed!"; private static final ThreadLocal transactionHolder = new ThreadLocal<>(); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java index 26524fdca1..0a3a37a5df 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java @@ -124,6 +124,7 @@ public class AgentConfigImpl extends BaseConfig implements AgentConfig { public static final String JAR_COLLECTOR = "jar_collector"; public static final String JMX = "jmx"; public static final String JFR = "jfr"; + public static final String OTEL = "opentelemetry"; public static final String KOTLIN_COROUTINES = "coroutines"; public static final String REINSTRUMENT = "reinstrument"; public static final String SLOW_SQL = "slow_sql"; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/database/DatabaseStatementParser.java b/newrelic-agent/src/main/java/com/newrelic/agent/database/DatabaseStatementParser.java index a376ffd623..396ada4eca 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/database/DatabaseStatementParser.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/database/DatabaseStatementParser.java @@ -11,6 +11,8 @@ import com.newrelic.agent.bridge.datastore.DatabaseVendor; +import javax.annotation.Nullable; + /** * Parses a sql string and returns a {@link ParsedDatabaseStatement}. * @@ -37,12 +39,11 @@ public interface DatabaseStatementParser { /** * Returns a parsed statement even if the statement is unparseable. Must not return null. - * * * @param databaseVendor * @param statement * @param resultSetMetaData */ ParsedDatabaseStatement getParsedDatabaseStatement(DatabaseVendor databaseVendor, String statement, - ResultSetMetaData resultSetMetaData); + @Nullable ResultSetMetaData resultSetMetaData); } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/database/SqlObfuscator.java b/newrelic-agent/src/main/java/com/newrelic/agent/database/SqlObfuscator.java index 5bcdf5a205..dcad33f6c4 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/database/SqlObfuscator.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/database/SqlObfuscator.java @@ -8,6 +8,7 @@ package com.newrelic.agent.database; import com.google.common.base.Joiner; +import com.newrelic.api.agent.QueryConverter; import jregex.Pattern; import java.util.HashMap; @@ -32,6 +33,17 @@ public abstract class SqlObfuscator { public static final String OBFUSCATED_SETTING = "obfuscated"; public static final String RAW_SETTING = "raw"; public static final String OFF_SETTING = "off"; + private final QueryConverter queryConverter = new QueryConverter() { + @Override + public String toRawQueryString(String rawQuery) { + return rawQuery; + } + + @Override + public String toObfuscatedQueryString(String rawQuery) { + return obfuscateSql(rawQuery); + } + }; private SqlObfuscator() { } @@ -61,6 +73,10 @@ public boolean isObfuscating() { return false; } + public QueryConverter getQueryConverter() { + return queryConverter; + } + static class DefaultSqlObfuscator extends SqlObfuscator { private static final Pattern ALL_DIALECTS_PATTERN; private static final Pattern ALL_UNMATCHED_PATTERN; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/InstrumentationImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/InstrumentationImpl.java index 0c6f8839da..ddbcf43017 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/InstrumentationImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/InstrumentationImpl.java @@ -34,6 +34,7 @@ import com.newrelic.agent.profile.v2.TransactionProfileSession; import com.newrelic.agent.reinstrument.PeriodicRetransformer; import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.agent.trace.TransactionGuidFactory; import com.newrelic.agent.tracers.ClassMethodSignature; import com.newrelic.agent.tracers.ClassMethodSignatures; import com.newrelic.agent.tracers.DefaultSqlTracer; @@ -50,6 +51,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import javax.annotation.Nullable; import java.io.Closeable; import java.lang.instrument.UnmodifiableClassException; import java.lang.reflect.Method; @@ -64,6 +66,7 @@ import static com.newrelic.agent.Transaction.SCALA_API_TRACER_FLAGS; import static com.newrelic.agent.Transaction.SCALA_API_TXN_CLASS_SIGNATURE_ID; +import static com.newrelic.agent.Transaction.GENERIC_TXN_CLASS_SIGNATURE_ID; public class InstrumentationImpl implements Instrumentation { @@ -135,7 +138,7 @@ public ExitTracer createTracer(Object invocationTarget, int signatureId, boolean * a Transaction is present on the thread. If present, we do not know if the Transaction has been started. */ @Override - public ExitTracer createTracer(Object invocationTarget, int signatureId, String metricName, int flags) { + public @Nullable ExitTracer createTracer(Object invocationTarget, int signatureId, String metricName, int flags) { try { if (ServiceFactory.getServiceManager().isStopped()) { return null; @@ -365,6 +368,11 @@ public ExitTracer createScalaTxnTracer() { return createTracer(null, SCALA_API_TXN_CLASS_SIGNATURE_ID, null, SCALA_API_TRACER_FLAGS); } + @Override + public @Nullable ExitTracer createTracer(String metricName, int flags) { + return createTracer(null, GENERIC_TXN_CLASS_SIGNATURE_ID, metricName, flags); + } + private boolean overSegmentLimit(TransactionActivity transactionActivity) { Transaction transaction; if (transactionActivity == null) { diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/pointcuts/XmlRpcPointCut.java b/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/pointcuts/XmlRpcPointCut.java index 09237cb75e..9077ff1903 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/pointcuts/XmlRpcPointCut.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/instrumentation/pointcuts/XmlRpcPointCut.java @@ -72,7 +72,7 @@ private XmlRpcTracer(PointCut pc, Transaction transaction, ClassMethodSignature this.library = library; } - private void finish() { + private void doFinish() { try { NewRelic.getAgent().getTracedMethod().reportAsExternal(HttpParameters .library(library) @@ -92,13 +92,13 @@ private void finish() { @Override public void finish(int opcode, Object returnValue) { - finish(); + doFinish(); super.finish(opcode, returnValue); } @Override public void finish(Throwable throwable) { - finish(); + doFinish(); super.finish(throwable); } } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java index ef51fa7d92..e6f2894be3 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/analytics/SpanEventFactory.java @@ -10,6 +10,7 @@ import com.google.common.base.Joiner; import com.newrelic.agent.attributes.AttributeNames; import com.newrelic.agent.attributes.AttributeValidator; +import com.newrelic.agent.bridge.datastore.SqlQueryConverter; import com.newrelic.agent.config.AgentConfig; import com.newrelic.agent.config.AttributesConfig; import com.newrelic.agent.database.SqlObfuscator; @@ -25,6 +26,7 @@ import com.newrelic.api.agent.ExternalParameters; import com.newrelic.api.agent.CloudParameters; import com.newrelic.api.agent.HttpParameters; +import com.newrelic.api.agent.QueryConverter; import com.newrelic.api.agent.MessageConsumeParameters; import com.newrelic.api.agent.MessageProduceParameters; import com.newrelic.api.agent.SlowQueryDatastoreParameters; @@ -481,12 +483,20 @@ private String determineObfuscationLevel(SlowQueryDatastoreParameters slo if (config.isHighSecurity() || config.getTransactionTracerConfig().getRecordSql().equals(SqlObfuscator.OFF_SETTING)) { return null; } else if (config.getTransactionTracerConfig().getRecordSql().equals(SqlObfuscator.RAW_SETTING)) { - return slowQueryDatastoreParameters.getQueryConverter().toRawQueryString(slowQueryDatastoreParameters.getRawQuery()); + return getQueryConverter(slowQueryDatastoreParameters).toRawQueryString(slowQueryDatastoreParameters.getRawQuery()); } else { - return slowQueryDatastoreParameters.getQueryConverter().toObfuscatedQueryString(slowQueryDatastoreParameters.getRawQuery()); + return getQueryConverter(slowQueryDatastoreParameters).toObfuscatedQueryString(slowQueryDatastoreParameters.getRawQuery()); } } + private static QueryConverter getQueryConverter(SlowQueryDatastoreParameters slowQueryDatastoreParameters) { + final QueryConverter queryConverter = slowQueryDatastoreParameters.getQueryConverter(); + if (queryConverter == SqlQueryConverter.INSTANCE) { + return (QueryConverter) ServiceFactory.getDatabaseService().getDefaultSqlObfuscator().getQueryConverter(); + } + return queryConverter; + } + public SpanEvent build() { builder.timestamp(timestampSupplier.get()); return builder.build(); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/tracers/AbstractTracer.java b/newrelic-agent/src/main/java/com/newrelic/agent/tracers/AbstractTracer.java index f31c432c04..46a95ce20b 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/tracers/AbstractTracer.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/tracers/AbstractTracer.java @@ -21,6 +21,7 @@ import com.newrelic.api.agent.ExternalParameters; import com.newrelic.api.agent.InboundHeaders; import com.newrelic.api.agent.OutboundHeaders; +import com.newrelic.api.agent.Token; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -382,6 +383,11 @@ public void setAttribute(String key, Object value, boolean checkLimits, boolean } } + @Override + public Token getToken() { + return getTransaction().getToken(); + } + static int sizeof(Object value) { int size = 0; if (value == null) { @@ -400,6 +406,16 @@ static int sizeof(Object value) { return size; } + @Override + public String getTraceId() { + return getTransaction().getSpanProxy().getOrCreateTraceId(); + } + + @Override + public String getSpanId() { + return getGuid(); + } + @Override public void setAgentAttribute(String key, Object value) { setAttribute(key, value, true, false, false); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/tracers/DefaultTracer.java b/newrelic-agent/src/main/java/com/newrelic/agent/tracers/DefaultTracer.java index 4bd1254c46..2ee3d5d069 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/tracers/DefaultTracer.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/tracers/DefaultTracer.java @@ -13,11 +13,13 @@ import com.newrelic.agent.TransactionActivity; import com.newrelic.agent.attributes.AttributeNames; import com.newrelic.agent.attributes.AttributeValidator; +import com.newrelic.agent.bridge.datastore.UnknownDatabaseVendor; import com.newrelic.agent.bridge.external.ExternalMetrics; import com.newrelic.agent.config.AgentConfigImpl; import com.newrelic.agent.config.DatastoreConfig; import com.newrelic.agent.config.TransactionTracerConfig; import com.newrelic.agent.database.DatastoreMetrics; +import com.newrelic.agent.database.ParsedDatabaseStatement; import com.newrelic.agent.database.SqlObfuscator; import com.newrelic.agent.service.ServiceFactory; import com.newrelic.agent.stats.ResponseTimeStats; @@ -746,8 +748,9 @@ private void recordExternalMetricsHttp(HttpParameters externalParameters) { private void recordExternalMetricsDatastore(DatastoreParameters datastoreParameters) { Transaction tx = getTransactionActivity().getTransaction(); if (tx != null && datastoreParameters != null) { + final String collection = getCollection(datastoreParameters); DatastoreMetrics.collectDatastoreMetrics(datastoreParameters.getProduct(), tx, this, - datastoreParameters.getCollection(), datastoreParameters.getOperation(), + collection, datastoreParameters.getOperation(), datastoreParameters.getHost(), datastoreParameters.getPort(), datastoreParameters.getPathOrId(), datastoreParameters.getDatabaseName()); @@ -775,6 +778,23 @@ private void recordExternalMetricsDatastore(DatastoreParameters datastoreParamet } } + private String getCollection(DatastoreParameters datastoreParameters) { + final String collection = datastoreParameters.getCollection(); + if (collection == null && datastoreParameters instanceof SlowQueryDatastoreParameters) { + final Object rawQuery = ((SlowQueryDatastoreParameters)datastoreParameters).getRawQuery(); + if (rawQuery != null) { + ParsedDatabaseStatement databaseStatement = ServiceFactory.getDatabaseService(). + getDatabaseStatementParser().getParsedDatabaseStatement( + UnknownDatabaseVendor.INSTANCE, rawQuery.toString(), null); + if (databaseStatement.recordMetric()) { + return databaseStatement.getModel(); + } + } + return "unknown"; + } + return collection; + } + private void catForMessaging(MessageProduceParameters produceParameters) { OutboundHeaders outboundHeaders = produceParameters.getOutboundHeaders(); if (outboundHeaders == null) { diff --git a/newrelic-agent/src/main/resources/newrelic.yml b/newrelic-agent/src/main/resources/newrelic.yml index a68f94aab4..c4c2d550f7 100644 --- a/newrelic-agent/src/main/resources/newrelic.yml +++ b/newrelic-agent/src/main/resources/newrelic.yml @@ -500,6 +500,51 @@ common: &default_settings # An example label #label_name: label_value + # Telemetry signals (Logs, Metrics, and Traces) emitted by OpenTelemetry APIs can + # be incorporated into the Java agent and controlled by the following config options. + opentelemetry: + + # Set to true to allow individual OpenTelemetry signals to be enabled, false to disable all OpenTelemetry signals. + # Default is false. + enabled: false + + # OpenTelemetry Logs signals. + logs: + + # Set to true to enable OpenTelemetry Logs signals. + # Default is false. + enabled: false + + # OpenTelemetry Metrics signals. + metrics: + + # Set to true to enable OpenTelemetry Metrics signals. + # Default is false. + enabled: false + + # A comma-delimited string of OpenTelemetry Meters (e.g. "MeterName1,MeterName2") whose signals should be included. + # By default, all Meters are included. This will override any default Meter excludes in the agent, effectively re-enabling them. + #include: + + # A comma-delimited string of OpenTelemetry Meters (e.g. "MeterName3,MeterName4") whose signals should be excluded. + # This takes precedence over all other includes/excludes sources, effectively disabling the listed Meters. + #exclude: + + # OpenTelemetry Traces signals. + traces: + + # Set to true to enable OpenTelemetry Traces signals. + # Default is false. + enabled: false + + # A comma-delimited string of OpenTelemetry Tracers (e.g. "TracerName1,TracerName2") whose signals should be included. + # By default, all Tracers are included. This will override any default Tracer excludes in the agent, effectively re-enabling them. + #include: + + # A comma-delimited string of OpenTelemetry Tracers (e.g. "TracerName3,TracerName4") whose signals should be excluded. + # This takes precedence over all other includes/excludes sources, effectively disabling the listed Tracers. + #exclude: + # New Relic Security vulnerability detection. security: # Determines whether the security data is sent to New Relic or not. When this is disabled and agent.enabled is diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/instrumentation/tracing/FlyweightTraceMethodVisitorTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/instrumentation/tracing/FlyweightTraceMethodVisitorTest.java index 754e54701b..8c1ec31a48 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/instrumentation/tracing/FlyweightTraceMethodVisitorTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/instrumentation/tracing/FlyweightTraceMethodVisitorTest.java @@ -24,9 +24,15 @@ public class FlyweightTraceMethodVisitorTest { @Test public void verifyTracedMethodStitching() { - // these methods are overridden in the bridge with a different signature. ignore them - Set excludes = ImmutableSet.of(new Method("getParentTracedMethod", - "()Lcom/newrelic/api/agent/TracedMethod;")); + Set excludes = ImmutableSet.of( + // this method is overridden in the bridge with a different signature. ignore it + new Method("getParentTracedMethod", + "()Lcom/newrelic/api/agent/TracedMethod;"), + // these methods have default implementations which are fine because flyweight tracers are leaves + new Method("getTraceId", + "()Ljava/lang/String;"), + new Method("getSpanId", + "()Ljava/lang/String;")); TraceDetails trace = TraceDetailsBuilder.newBuilder().build(); FlyweightTraceMethodVisitor mv = new FlyweightTraceMethodVisitor("", null, 0, "go", "()V", trace, null); diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/tracers/DefaultTracerTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/tracers/DefaultTracerTest.java index 49e7ea498f..f0c1e3d3be 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/tracers/DefaultTracerTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/tracers/DefaultTracerTest.java @@ -25,6 +25,7 @@ import com.newrelic.agent.bridge.Token; import com.newrelic.agent.bridge.TransactionNamePriority; import com.newrelic.agent.bridge.datastore.DatastoreVendor; +import com.newrelic.agent.bridge.datastore.SqlQueryConverter; import com.newrelic.agent.config.AgentConfigFactory; import com.newrelic.agent.config.TransactionTracerConfig; import com.newrelic.agent.database.SqlObfuscator; @@ -111,6 +112,7 @@ public void before() throws Exception { APP_NAME = ServiceFactory.getConfigService().getDefaultAgentConfig().getApplicationName(); SamplingPriorityQueue eventPool = spanEventService.getOrCreateDistributedSamplingReservoir(APP_NAME); eventPool.clear(); + ServiceFactory.getStatsService().getStatsEngineForHarvest("Unit Test").clear(); } @Test @@ -609,6 +611,24 @@ public void testDatastoreParametersNoHost() { assertClmAbsent(tracer); } + @Test + public void testDatastoreParametersNullCollection() { + DefaultTracer tracer = prepareTracer(); + TransactionStats stats = tracer.getTransactionActivity().getTransactionStats(); + + tracer.reportAsExternal(DatastoreParameters + .product("Product") + .collection(null) + .operation("operation") + .noInstance() + .noDatabaseName().slowQuery("SELECT * FROM users", SqlQueryConverter.INSTANCE) + .build()); + tracer.recordMetrics(stats); + // verify that collection is parsed from SQL + assertEquals("Datastore/statement/Product/users/operation", tracer.getMetricName()); + assertClmAbsent(tracer); + } + @Test public void testNoParametersInUri() { DefaultTracer tracer = prepareTracer(); diff --git a/newrelic-weaver-api/src/main/java/com/newrelic/api/agent/weaver/MatchType.java b/newrelic-weaver-api/src/main/java/com/newrelic/api/agent/weaver/MatchType.java index b4251471a5..be2fc652df 100644 --- a/newrelic-weaver-api/src/main/java/com/newrelic/api/agent/weaver/MatchType.java +++ b/newrelic-weaver-api/src/main/java/com/newrelic/api/agent/weaver/MatchType.java @@ -24,6 +24,9 @@ public enum MatchType { /** * The weave instrumentation will be injected into all classes which implement an interface with the exact same name * as the weave class. + * + * To instrument a `default` method on an interface, define the instrumentation + * class as `public abstract` and define the target method as `public`. */ Interface(false);