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..fff5b70b91 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/README.md @@ -0,0 +1,226 @@ +# OpenTelemetry Instrumentation + +This instrumentation module weaves parts of the OpenTelemetry SDK to incorporate bits of OpenTelemetry functionality into the New Relic Java agent. + +Specifically, it can: + +* Detect OpenTelemetry Spans and add them to New Relic Java agent traces as New Relic Spans. +* Detect OpenTelemetry LogRecords and report them as New Relic LogEvents. +* Detect OpenTelemetry dimensional metrics and report them to the APM entity being monitored by the Java agent. +* Autoconfigure the OpenTelemetry SDK so that OpenTelemetry data is sent to New Relic and properly associated with an APM entity guid. + +## OpenTelemetry Configuration + +To use the OpenTelemetry functionality incorporated into the New Relic Java agent you must enable the following config options: + +```commandline +-Dotel.java.global-autoconfigure.enabled=true +``` + +## New Relic Java Agent Configuration + +To use the OpenTelemetry Span and dimensional metric functionality incorporated into the New Relic Java agent you must enable the following config options: + +Configuration via yaml: + +```yaml + opentelemetry: + # config to enable different types of telemetry from the OpenTelemetry SDK + sdk: + autoconfigure: + enabled: true + spans: + enabled: true + logs: + enabled: true + # instrumentation scope names which are excluded from reporting traces and logs + instrumentation: + specific-instrumentation-scope-name-1: + enabled: true + specific-instrumentation-scope-name-2: + enabled: true + metrics: + # comma-separated list of meter names which are excluded from reporting metrics + exclude: foo-module,bar-module +``` + +Configuration via system property: + +``` +-Dnewrelic.config.opentelemetry.sdk.autoconfigure.enabled=true +-Dnewrelic.config.opentelemetry.sdk.spans.enabled=true +-Dnewrelic.config.opentelemetry.sdk.logs.enabled=true + +# instrumentation scope names which are excluded from reporting traces and logs +-Dnewrelic.config.opentelemetry.instrumentation.[SPECIFIC_INSTRUMENTATION_SCOPE_NAME].enabled=true + +# comma-separated list of meter names which are excluded from reporting metrics +-Dnewrelic.config.opentelemetry.metrics.exclude=foo-module,bar-module +``` + +Configuration via environment variable: + +``` +NEW_RELIC_OPENTELEMETRY_SDK_AUTOCONFIGURE_ENABLED=true +NEW_RELIC_OPENTELEMETRY_SDK_SPANS_ENABLED=true +NEW_RELIC_OPENTELEMETRY_SDK_LOGS_ENABLED=true + +# instrumentation scope names which are excluded from reporting traces and logs +NEW_RELIC_OPENTELEMETRY_INSTRUMENTATION_[SPECIFIC_INSTRUMENTATION_SCOPE_NAME]_ENABLED=true + +# comma-separated list of meter names which are excluded from reporting metrics +NEW_RELIC_OPENTELEMETRY_METRICS_EXCLUDE=foo-module,bar-module +``` + +## OpenTelemetry Dimensional Metrics + +OpenTelemetry APIs can be used to create dimensional metrics which will be detected by the New Relic Java agent and reported to the APM entity being monitored +by the New Relic Java agent. + +To use this functionality, enable the feature as documented above, add the required `opentelemetry` dependencies to your application: + +```groovy +implementation(platform("io.opentelemetry:opentelemetry-bom:1.44.1")) +implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") +implementation("io.opentelemetry:opentelemetry-exporter-otlp") +``` + +Then utilize the OpenTelemetry APIs to record dimensional metrics: + +```java +LongCounter longCounter = GlobalOpenTelemetry.get().getMeterProvider().get("my-application").counterBuilder("my.application.counter").build(); +longCounter. + +add(1,Attributes.of(AttributeKey.stringKey("foo"), "bar")); +``` + +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 Spans + +Documented below are several approaches for incorporating OpenTelemetry 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 Instrumentation + +The New Relic Java agent will detect Spans emitted by [OpenTelemetry instrumentation](https://opentelemetry.io/docs/languages/java/instrumentation/). 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. + +Currently, the New Relic Java agent does not load any OpenTelemetry instrumentation it simply detects Spans emitted by OpenTelemetry manual instrumentation, +native instrumentation, library instrumentation, or zero code instrumentation (i.e. bytecode instrumentation that would also require running the OpenTelemetry +Java agent). + +Depending on the OpenTelemetry Span `SpanKind`, it may result in the New Relic Java agent starting a transaction (when one doesn't already exist). + +* `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 + +OpenTelemetry APIs can be used to create log records which will be detected by the New Relic Java agent and reported to the APM entity being monitored by the +New Relic Java agent. The log records will be reported as log events in New Relic, which will be associated with a transaction if the logging occurred within +one. + +To use this functionality, enable the feature as documented above, add the required `opentelemetry` dependencies to your application: + +```groovy + implementation(platform("io.opentelemetry:opentelemetry-bom:1.28.0")) +implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") +implementation("io.opentelemetry:opentelemetry-exporter-logging") +``` + +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..b75a22d96c 100644 --- a/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/build.gradle +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/build.gradle @@ -1,16 +1,64 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + plugins { id("java") } +configurations { + shadowIntoJar +} +configurations.implementation.extendsFrom(configurations.shadowIntoJar) + dependencies { implementation(project(":agent-bridge")) implementation(project(":newrelic-api")) implementation(project(":newrelic-weaver-api")) + implementation(project(":newrelic-agent")) implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.28.0") + + 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") +} + +/** + * 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" } 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/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/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..25b9c02262 --- /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,171 @@ +/* + * + * * 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.AppLoggingUtils; +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.LOGGER_FQCN; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.LOGGER_NAME; +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 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( + OTEL_EXCEPTION_MESSAGE.getKey(), + OTEL_EXCEPTION_TYPE.getKey(), + OTEL_EXCEPTION_STACKTRACE.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)) { + 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); + } + } + logEventMap.put(TIMESTAMP, logRecordData.getTimestampEpochNanos()); + + // 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 (AppLoggingUtils.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) { + String severityName = severity.name(); + if (severityName.isEmpty()) { + logEventMap.put(LEVEL, UNKNOWN); + } else { + logEventMap.put(LEVEL, severityName); + } + } + + String threadName = ((NRLogRecord.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 AppLoggingUtils.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..fceb1c9a2b --- /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,32 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.nr.agent.instrumentation.utils.span; + +public class AttributeKey { + private final String key; + private final String semanticConvention; + private final String version; + + AttributeKey(String key, String semanticConventionString) { + this.key = key; + String [] conventionNameAndVersion = semanticConventionString.split(":"); + this.semanticConvention = conventionNameAndVersion[0]; + this.version = conventionNameAndVersion[1]; + } + + public String getKey() { + return key; + } + + public String getVersion() { + return version; + } + + public String getSemanticConvention() { + return semanticConvention; + } +} 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..39e7e634fe --- /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,123 @@ +/* + * + * * 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 io.opentelemetry.api.trace.SpanKind; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; + +public class AttributeMapper { + private static final String MAPPING_RESOURCE = "attribute-mappings.json"; + + 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 (InputStream inputStream = AttributeMapper.class.getClassLoader().getResourceAsStream(MAPPING_RESOURCE); + InputStreamReader reader = new InputStreamReader(inputStream)) { + + instance = new AttributeMapper(); + + JSONParser parser = new JSONParser(); + JSONArray rootArray = (JSONArray) parser.parse(reader); + + 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... + NewRelic.getAgent().getLogger().log(Level.SEVERE, "Unable to read OTel attribute mappings", e); + } + } + } + } + + 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); + } +} 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..9260ea9304 --- /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,11 @@ +/* + * + * * 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 +} 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..c9c80c037f 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,6 +14,10 @@ import java.util.logging.Level; +/** + * Weaved to autoconfigure the OpenTelemetrySDK properties + * and resources for compatability with New Relic. + */ @Weave(type = MatchType.ExactClass) public class AutoConfiguredOpenTelemetrySdk { @@ -20,11 +31,12 @@ public class AutoConfiguredOpenTelemetrySdk { */ public static AutoConfiguredOpenTelemetrySdkBuilder builder() { final AutoConfiguredOpenTelemetrySdkBuilder builder = Weaver.callOriginal(); - Boolean autoConfigure = NewRelic.getAgent().getConfig().getValue("opentelemetry.sdk.autoconfigure.enabled", false); + final Boolean autoConfigure = NewRelic.getAgent().getConfig().getValue("opentelemetry.sdk.autoconfigure.enabled", false); if (autoConfigure == null || autoConfigure) { 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); } 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..20f18c9b32 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySDKCustomizer.java @@ -0,0 +1,117 @@ +/* + * + * * 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.agent.config.OtelConfig; +import com.newrelic.agent.service.ServiceFactory; +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; + +/** + * 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.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(), ServiceFactory.getConfigService().getDefaultAgentConfig().getOtelConfig()); + } + + static SdkMeterProviderBuilder applyMeterExcludes(SdkMeterProviderBuilder sdkMeterProviderBuilder, Agent agent, OtelConfig otelConfig) { + final List excludedMeters = otelConfig.getExcludedMeters(); + 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..c9e11400e2 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLogRecord.java @@ -0,0 +1,282 @@ +/* + * + * * 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"); + + 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; + // Capture thread information at the time of log record creation, OTel does not provide this + this.threadId = Thread.currentThread().getId(); + this.threadName = Thread.currentThread().getName(); + } + + 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..bac84c7b44 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLogRecordBuilder.java @@ -0,0 +1,183 @@ +/* + * + * * 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.Config; +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); + } + } + } + + /** + * Determines if the LogRecordBuilder should be enabled based on the configuration. + * If auto-configuration is enabled and logs are not explicitly disabled, then + * the LogRecordBuilder is enabled. + * + * @param config the agent configuration + * @return true if NRLogRecordBuilder should be used, false otherwise + */ + static boolean isLogRecordBuilderEnabled(Config config) { + final Boolean autoConfigure = config.getValue("opentelemetry.sdk.autoconfigure.enabled", false); + if (autoConfigure == null || autoConfigure) { + // When opentelemetry.sdk.logs.enabled=false, this + // setting will prevent NR LogEvents from being created, + // without preventing OTel LogRecords from being emitted. + final Boolean logsEnabled = config.getValue("opentelemetry.sdk.logs.enabled", + true); // TODO: Verify default value. This is added so it doesn't return null or cause a class cast exception from String to Boolean. + return logsEnabled == null || logsEnabled; + } + return false; + } +} 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..a9930fcd1a --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/logs/NRLoggerBuilder.java @@ -0,0 +1,69 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import com.newrelic.api.agent.Config; +import io.opentelemetry.api.OpenTelemetry; +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 final Config config; + private String schemaUrl; + private String instrumentationScopeVersion; + + public NRLoggerBuilder(Config config, String instrumentationScopeName, LoggerSharedState sharedState) { + this.config = config; + 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() { + Boolean enabled = config.getValue( + "opentelemetry.instrumentation." + instrumentationScopeName + ".enabled", true); // TODO: Verify default value. This is added so it doesn't return null or cause a class cast exception from String to Boolean. + if (enabled != null && !enabled) { + /* + * This will return a no-op Logger, which results in + * no OTel LogRecords being emitted, meaning that the + * customer's logs from OTel APIs will not be written anywhere. + * + * If the goal is to prevent NR LogEvents from being created, + * but continue to emit OTel LogRecords, then that should be + * done by disabling the logs functionality in agent config: + * + * opentelemetry: + * sdk: + * logs: + * enabled: false + */ + return OpenTelemetry.noop().getLogsBridge().get(instrumentationScopeName); + } else { + 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..8bf853337c --- /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.Config; +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; + +/** + * 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(); + Config config = NewRelic.getAgent().getConfig(); + + // Generate the instrumentation module supportability metric + NewRelic.incrementCounter("Supportability/Logging/Java/OpenTelemetryBridge/enabled"); + + if (NRLogRecordBuilder.isLogRecordBuilderEnabled(config)) { + // return our logger builder instead of the OTel instance + return new NRLoggerBuilder(config, instrumentationNameOrDefault(instrumentationScopeName), sharedState); + } + 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..a72a95dfa2 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/ExitTracerSpan.java @@ -0,0 +1,428 @@ +/* + * + * * 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 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.Arrays; +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 DB_NAME = AttributeKey.stringKey("db.name"); + + private static final AttributeKey SERVER_ADDRESS = AttributeKey.stringKey("server.address"); + private static final AttributeKey SERVER_PORT = AttributeKey.longKey("server.port"); + private static final AttributeKey URL_FULL = AttributeKey.stringKey("url.full"); + private static final AttributeKey URL_SCHEME = AttributeKey.stringKey("url.scheme"); + private static final AttributeKey RPC_METHOD = AttributeKey.stringKey("rpc.method"); + private static final AttributeKey CODE_FUNCTION = AttributeKey.stringKey("code.function"); + private static final AttributeKey HTTP_REQUEST_METHOD = AttributeKey.stringKey("http.request.method"); + + private static final List> PROCEDURE_KEYS = + Arrays.asList(CODE_FUNCTION, RPC_METHOD, HTTP_REQUEST_METHOD); + // 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; + + 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(DB_SYSTEM); + if (dbSystem != null) { + String operation = getAttribute(DB_OPERATION); + DatastoreParameters.InstanceParameter builder = DatastoreParameters + .product(dbSystem) + .collection(getAttribute(DB_SQL_TABLE)) + .operation(operation == null ? "unknown" : operation); + String serverAddress = getAttribute(SERVER_ADDRESS); + Long serverPort = getAttribute(SERVER_PORT); + + DatastoreParameters.DatabaseParameter instance = serverAddress == null ? builder.noInstance() : + builder.instance(serverAddress, (serverPort == null ? Long.valueOf(0L) : serverPort).intValue()); + + String dbName = getAttribute(DB_NAME); + DatastoreParameters.SlowQueryParameter slowQueryParameter = + dbName == null ? instance.noDatabaseName() : instance.databaseName(dbName); + final String dbStatement = getAttribute(DB_STATEMENT); + 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); + } + } + } + + String getProcedure() { + for (AttributeKey key : PROCEDURE_KEYS) { + String value = getAttribute(key); + if (value != null) { + return value; + } + } + return "unknown"; + } + + URI getUri() throws URISyntaxException { + final String urlFull = getAttribute(URL_FULL); + if (urlFull != null) { + return URI.create(urlFull); + } else { + final String serverAddress = getAttribute(SERVER_ADDRESS); + if (serverAddress != null) { + final String scheme = getAttribute(URL_SCHEME); + final Long serverPort = getAttribute(SERVER_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..ab428c6857 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRSpanBuilder.java @@ -0,0 +1,340 @@ +/* + * + * * 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.Config; +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 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; + + 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 -> { + }; + } + } + + /** + * Determines if the SpanBuilder should be enabled based on the configuration. + * If auto-configuration is enabled and spans are not explicitly disabled, then + * the SpanBuilder is enabled. + * + * @param config The configuration to check. + * @return true if the SpanBuilder should be enabled, false otherwise. + */ + static boolean isSpanBuilderEnabled(Config config) { + final Boolean autoConfigure = config.getValue("opentelemetry.sdk.autoconfigure.enabled", false); + if (autoConfigure == null || autoConfigure) { + final Boolean spansEnabled = config.getValue("opentelemetry.sdk.spans.enabled", true); // TODO: Verify default value. This is added so it doesn't return null or cause a class cast exception from String to Boolean. + return spansEnabled == null || spansEnabled; + } + return false; + } + + @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("http.route"); + if (httpRoute != null) { + return httpRoute.toString(); + } + return (String) attributes.get("url.path"); + } + + @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("user_agent.original"); + } + // 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("http.request.method"); + } + }; + + final ExtendedResponse response = new ExtendedResponse() { + + @Override + public int getStatus() throws Exception { + Object statusCode = attributes.get("http.response.status_code"); + 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..588d46ceb3 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/NRTracerBuilder.java @@ -0,0 +1,61 @@ +/* + * + * * 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.api.agent.Config; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerBuilder; + +/** + * 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 final Config config; + private String schemaUrl; + private String instrumentationScopeVersion; + + public NRTracerBuilder(Config config, String instrumentationScopeName, TracerSharedState sharedState) { + this.config = config; + 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 instrumentation is disabled in the configuration, returns a noop tracer. + */ + @Override + public Tracer build() { + Boolean enabled = config.getValue( + "opentelemetry.instrumentation." + instrumentationScopeName + ".enabled", + true); // TODO: Verify default value. This is added so it doesn't return null or cause a class cast exception from String to Boolean. + if (enabled != null && !enabled) { + 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..b41c343f78 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider_Instrumentation.java @@ -0,0 +1,33 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.api.agent.Config; +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; + +/** + * 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(); + Config config = NewRelic.getAgent().getConfig(); + if (NRSpanBuilder.isSpanBuilderEnabled(config)) { + // return our tracer builder instead of the OTel instance + return new NRTracerBuilder(config, instrumentationScopeName, sharedState); + } + 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..400d0db59a --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/main/resources/attribute-mappings.json @@ -0,0 +1,20 @@ +[ + { + "spanKind": "SERVER", + "attributeTypes": [ + { + "attributeType": "Port", + "attributes": [ + { + "name": "server.port", + "version": "HTTP-Server:1.23" + }, + { + "name": "net.host.port", + "version": "HTTP-Server:1.20" + } + ] + } + ] + } +] \ 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/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..a7f9a18225 --- /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,61 @@ +/* + * + * * 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(2, attributeTypesBySpan.size()); + + // The actual attribute key mappings... + for (AttributeType attributeType : AttributeType.values()) { + assertEquals(2, attributeTypesBySpan.get(attributeType).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)); + } +} 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..9813b3ca55 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/autoconfigure/OpenTelemetrySDKCustomizerTest.java @@ -0,0 +1,178 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.autoconfigure; + +import com.newrelic.agent.config.OtelConfig; +import com.newrelic.api.agent.Agent; +import com.newrelic.api.agent.Config; +import com.newrelic.api.agent.Logger; +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 java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +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); + List expectedExclude = Arrays.asList("drop-me", "drop-me-too", "never-used"); + + SdkMeterProvider provider = setupProviderFromExcludesConfig(reader, expectedExclude); + + //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); + List noExcludedMeters = new ArrayList<>(); + SdkMeterProvider provider = setupProviderFromExcludesConfig(reader, noExcludedMeters); + + //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, List excludedMeters) { + Agent agent = mock(Agent.class); + Logger logger = mock(Logger.class); + when(agent.getLogger()).thenReturn(logger); + OtelConfig otelConfig = mock(OtelConfig.class); + when(otelConfig.getExcludedMeters()).thenReturn(excludedMeters); + SdkMeterProviderBuilder customizedBuilder = OpenTelemetrySDKCustomizer.applyMeterExcludes( + SdkMeterProvider.builder().registerMetricReader(reader), agent, otelConfig + ); + 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/NRLogRecordBuilderTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/NRLogRecordBuilderTest.java new file mode 100644 index 0000000000..b834ad3ef3 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/NRLogRecordBuilderTest.java @@ -0,0 +1,42 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import com.newrelic.api.agent.Config; +import junit.framework.TestCase; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NRLogRecordBuilderTest extends TestCase { + + public void testIsLogRecordBuilderEnabled() { + assertTrue(NRLogRecordBuilder.isLogRecordBuilderEnabled(createConfig(null, null))); + assertTrue(NRLogRecordBuilder.isLogRecordBuilderEnabled(createConfig(true, null))); + assertTrue(NRLogRecordBuilder.isLogRecordBuilderEnabled(createConfig(null, true))); + assertTrue(NRLogRecordBuilder.isLogRecordBuilderEnabled(createConfig(true, true))); + } + + public void testIsLogRecordBuilderDisabled() { + assertFalse(NRLogRecordBuilder.isLogRecordBuilderEnabled(createConfig(false, true))); + assertFalse(NRLogRecordBuilder.isLogRecordBuilderEnabled(createConfig(false, null))); + assertFalse(NRLogRecordBuilder.isLogRecordBuilderEnabled(createConfig(null, false))); + assertFalse(NRLogRecordBuilder.isLogRecordBuilderEnabled(createConfig(true, false))); + } + + private Config createConfig(Boolean autoconfigureEnabled, Boolean logsEnabled) { + Config config = mock(Config.class); + if (autoconfigureEnabled != null) { + when(config.getValue("opentelemetry.sdk.autoconfigure.enabled", false)).thenReturn(autoconfigureEnabled); + } + if (logsEnabled != null) { + when(config.getValue("opentelemetry.sdk.logs.enabled", true)).thenReturn(logsEnabled); + } + return config; + } +} 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..650099e2df --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/logs/NRLoggerBuilderTest.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright 2025 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.logs; + +import com.newrelic.api.agent.Config; +import com.newrelic.api.agent.NewRelic; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.resources.Resource; +import junit.framework.TestCase; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +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(NewRelic.getAgent().getConfig(), "test-lib", LOGGER_SHARED_STATE).build(); + + assertTrue(logger.getClass().getName(), logger.getClass().getName().startsWith( + "io.opentelemetry.sdk.logs.NRLoggerBuilder")); + } + + public void testBuildDisabled() { + Config config = mock(Config.class); + when(config.getValue("opentelemetry.instrumentation.test-lib.enabled", true)).thenReturn(false); + Logger logger = new NRLoggerBuilder(config, "test-lib", LOGGER_SHARED_STATE).build(); + assertSame(OpenTelemetry.noop().getLogsBridge().get("test-lib"), logger); + } +} 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/NRSpanBuilderTest.java b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/NRSpanBuilderTest.java new file mode 100644 index 0000000000..4f9ce3aca3 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/NRSpanBuilderTest.java @@ -0,0 +1,47 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.api.agent.Config; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NRSpanBuilderTest { + + @Test + public void testIsSpanBuilderEnabled() { + assertTrue(NRSpanBuilder.isSpanBuilderEnabled(createConfig(null, null))); + assertTrue(NRSpanBuilder.isSpanBuilderEnabled(createConfig(true, null))); + assertTrue(NRSpanBuilder.isSpanBuilderEnabled(createConfig(null, true))); + assertTrue(NRSpanBuilder.isSpanBuilderEnabled(createConfig(true, true))); + } + + @Test + public void testIsSpanBuilderDisabled() { + assertFalse(NRSpanBuilder.isSpanBuilderEnabled(createConfig(false, true))); + assertFalse(NRSpanBuilder.isSpanBuilderEnabled(createConfig(false, null))); + assertFalse(NRSpanBuilder.isSpanBuilderEnabled(createConfig(null, false))); + assertFalse(NRSpanBuilder.isSpanBuilderEnabled(createConfig(true, false))); + } + + private Config createConfig(Boolean autoconfigureEnabled, Boolean spansEnabled) { + Config config = mock(Config.class); + if (autoconfigureEnabled != null) { + when(config.getValue("opentelemetry.sdk.autoconfigure.enabled", false)).thenReturn(autoconfigureEnabled); + } + if (spansEnabled != null) { + when(config.getValue("opentelemetry.sdk.spans.enabled", true)).thenReturn(spansEnabled); + } + return config; + } + +} \ 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..299fa29b5e --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/java/io/opentelemetry/sdk/trace/NRTracerBuilderTest.java @@ -0,0 +1,42 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package io.opentelemetry.sdk.trace; + +import com.newrelic.api.agent.Config; +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 java.util.Collections; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +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(NewRelic.getAgent().getConfig(), "test-lib", + TRACER_SHARED_STATE).build(); + assertTrue(tracer.getClass().getName(), tracer.getClass().getName().startsWith( + "io.opentelemetry.sdk.trace.NRTracerBuilder")); + } + + public void testBuildDisabled() { + Config config = mock(Config.class); + when(config.getValue("opentelemetry.instrumentation.test-lib.enabled", true)).thenReturn(false); + Tracer tracer = new NRTracerBuilder(config, "test-lib", + TRACER_SHARED_STATE).build(); + assertSame(OpenTelemetry.noop().getTracer("dude"), tracer); + } +} \ No newline at end of file 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/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..8bbb4a233c --- /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" + }, + { + "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": "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": "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..54d48b6cf9 --- /dev/null +++ b/instrumentation/opentelemetry-sdk-extension-autoconfigure-1.28.0/src/test/resources/distributed_tracing.yml @@ -0,0 +1,12 @@ +common: &default_settings + distributed_tracing: + enabled: true + + opentelemetry: + sdk: + autoconfigure: + enabled: true + spans: + enabled: true + logs: + enabled: true 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 3f04ac038d..c156a64407 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java @@ -130,7 +130,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/AgentConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java index d43267ce20..7560877b10 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java @@ -155,6 +155,8 @@ public interface AgentConfig extends com.newrelic.api.agent.Config, DataSenderCo */ JarCollectorConfig getJarCollectorConfig(); + OtelConfig getOtelConfig(); + /** * Gets the Reinstrumentation configuration settings. * 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 f3d8606fca..3458f209d8 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 @@ -120,6 +120,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 REINSTRUMENT = "reinstrument"; public static final String SLOW_SQL = "slow_sql"; public static final String SPAN_EVENTS = "span_events"; @@ -265,6 +266,7 @@ public class AgentConfigImpl extends BaseConfig implements AgentConfig { private final KeyTransactionConfig keyTransactionConfig; private final LabelsConfig labelsConfig; private final NormalizationRuleConfig normalizationRuleConfig; + private final OtelConfig otelConfig; private final ReinstrumentConfig reinstrumentConfig; private final TransactionTracerConfigImpl requestTransactionTracerConfig; private final SlowTransactionsConfig slowTransactionsConfig; @@ -375,6 +377,7 @@ private AgentConfigImpl(Map props) { normalizationRuleConfig = new NormalizationRuleConfig(props); slowTransactionsConfig = initSlowTransactionsConfig(); obfuscateJvmPropsConfig = initObfuscateJvmPropsConfig(); + otelConfig = initOtelConfig(); agentControlIntegrationConfig = initAgentControlHealthCheckConfig(); Map flattenedProps = new HashMap<>(); @@ -795,6 +798,11 @@ private ObfuscateJvmPropsConfig initObfuscateJvmPropsConfig() { return new ObfuscateJvmPropsConfigImpl(props); } + private OtelConfig initOtelConfig() { + Map props = nestedProps(OTEL); + return new OtelConfig(props); + } + private CircuitBreakerConfig initCircuitBreakerConfig() { Map props = nestedProps(CircuitBreakerConfig.PROPERTY_NAME); return new CircuitBreakerConfig(props); @@ -1255,6 +1263,11 @@ public ObfuscateJvmPropsConfig getObfuscateJvmPropsConfig() { return obfuscateJvmPropsConfig; } + @Override + public OtelConfig getOtelConfig() { + return otelConfig; + } + @Override public ReinstrumentConfig getReinstrumentConfig() { return reinstrumentConfig; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/OtelConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/OtelConfig.java new file mode 100644 index 0000000000..f9d26dcf21 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/OtelConfig.java @@ -0,0 +1,25 @@ +package com.newrelic.agent.config; + +import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.api.agent.NewRelic; + +import java.util.List; +import java.util.Map; + +public class OtelConfig extends BaseConfig{ + + public static final String EXCLUDE_METERS = "metrics.exclude"; + + public static final String SYSTEM_PROPERTY_ROOT = "newrelic.config.opentelemetry."; + + private final List excludedMeters; + + public OtelConfig(Map props) { + super(props, SYSTEM_PROPERTY_ROOT); + excludedMeters = getUniqueStrings(EXCLUDE_METERS); + } + + public List getExcludedMeters(){ + return excludedMeters; + } +} 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 73368f8921..a70e2f5c50 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; @@ -486,12 +488,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/test/java/com/newrelic/agent/config/OtelConfigTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/config/OtelConfigTest.java new file mode 100644 index 0000000000..e273049bff --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/config/OtelConfigTest.java @@ -0,0 +1,75 @@ +package com.newrelic.agent.config; + +import static org.junit.Assert.*; + +import com.google.common.collect.ImmutableMap; +import com.newrelic.agent.SaveSystemPropertyProviderRule.TestEnvironmentFacade; +import com.newrelic.agent.SaveSystemPropertyProviderRule.TestSystemProps; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class OtelConfigTest { + + private Map configProps; + + private static final String ENV_VAR_METRICS_EXCLUDE = "NEW_RELIC_OPENTELEMETRY_METRICS_EXCLUDE"; + + private static final String SYSTEM_PROP_METRICS_EXCLUDE = "newrelic.config.opentelemetry.metrics.exclude"; + @Before + public void setup(){ + configProps = new HashMap<>(); + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new TestSystemProps(), + new TestEnvironmentFacade() + )); + } + + @Test + public void testDefaultConfigValues() { + OtelConfig config = new OtelConfig(configProps); + assertTrue(config.getExcludedMeters().isEmpty()); + } + + @Test + public void testGetExcludedMetersFromProps(){ + configProps.put("metrics.exclude", "foo,bar,baz"); + OtelConfig config = new OtelConfig(configProps); + assertEquals(3, config.getExcludedMeters().size()); + assertTrue(config.getExcludedMeters().contains("foo")); + assertTrue(config.getExcludedMeters().contains("bar")); + assertTrue(config.getExcludedMeters().contains("baz")); + } + + @Test + public void testExcludeMetersFromEnvVars(){ + TestEnvironmentFacade environmentFacade = new TestEnvironmentFacade(ImmutableMap.of( + ENV_VAR_METRICS_EXCLUDE, "hello,foo-bar,goodbye" + )); + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new TestSystemProps(), + environmentFacade + )); + OtelConfig config = new OtelConfig(configProps); + assertEquals(3, config.getExcludedMeters().size()); + assertTrue(config.getExcludedMeters().contains("hello")); + assertTrue(config.getExcludedMeters().contains("foo-bar")); + assertTrue(config.getExcludedMeters().contains("goodbye")); + } + + @Test + public void testExcludeMetersFromSystemProps(){ + Properties props = new Properties(); + props.put(SYSTEM_PROP_METRICS_EXCLUDE, "apple,banana"); + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( new TestSystemProps(props), new TestEnvironmentFacade())); + OtelConfig config = new OtelConfig(configProps); + assertEquals(2, config.getExcludedMeters().size()); + assertTrue(config.getExcludedMeters().contains("apple")); + assertTrue(config.getExcludedMeters().contains("banana")); + } + + +} \ No newline at end of file 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);