diff --git a/bootstrap/build.gradle b/bootstrap/build.gradle index 44cfc8c79..c4cbbed97 100644 --- a/bootstrap/build.gradle +++ b/bootstrap/build.gradle @@ -7,5 +7,8 @@ compileJava { } dependencies { - implementation "org.slf4j:slf4j-api:1.7.30" + // slf4j is included in the otel javaagent, no need to add it here too + compileOnly "org.slf4j:slf4j-api:1.7.30" + // add micrometer to the bootstrap classloader so that it's available in instrumentations + implementation "io.micrometer:micrometer-core:${versions.micrometer}" } diff --git a/bootstrap/src/main/java/com/splunk/opentelemetry/javaagent/bootstrap/GlobalMetricsTags.java b/bootstrap/src/main/java/com/splunk/opentelemetry/javaagent/bootstrap/GlobalMetricsTags.java new file mode 100644 index 000000000..4dd21e3a3 --- /dev/null +++ b/bootstrap/src/main/java/com/splunk/opentelemetry/javaagent/bootstrap/GlobalMetricsTags.java @@ -0,0 +1,49 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.javaagent.bootstrap; + +import io.micrometer.core.instrument.Tag; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class holds the list of tags that should be added to all meters registered by the javaagent. + * It needs to be loaded by the bootstrap classloader to be accessible inside instrumentations. + */ +public final class GlobalMetricsTags { + private static final Logger log = LoggerFactory.getLogger(GlobalMetricsTags.class); + + private static final List EMPTY = Collections.emptyList(); + private static final AtomicReference> INSTANCE = new AtomicReference<>(EMPTY); + + public static void set(List globalTags) { + List globalTagsCopy = Collections.unmodifiableList(new ArrayList<>(globalTags)); + if (!INSTANCE.compareAndSet(EMPTY, globalTagsCopy)) { + log.warn("GlobalMetricTags#set() was already called before"); + } + } + + public static List get() { + return INSTANCE.get(); + } + + private GlobalMetricsTags() {} +} diff --git a/build.gradle b/build.gradle index 0f3a6bd57..aa4407ec8 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,8 @@ def versions = [ 'opentelemetryJavaagent' : snapshot ? "1.1.0-SNAPSHOT" : '1.0.0', 'opentelemetryJavaagentAlpha': snapshot ? "1.1.0-alpha-SNAPSHOT" : '1.0.0-alpha', 'mockito' : '3.8.0', - 'jupiter' : '5.7.1' + 'jupiter' : '5.7.1', + 'micrometer' : '1.6.4' ] subprojects { diff --git a/custom/build.gradle b/custom/build.gradle index 8e0e89e8d..fb0b0a145 100644 --- a/custom/build.gradle +++ b/custom/build.gradle @@ -10,15 +10,28 @@ def relocatePackages = ext.relocatePackages dependencies { compileOnly(project(":bootstrap")) - implementation("io.opentelemetry:opentelemetry-sdk:${versions["opentelemetry"]}") - implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${versions.opentelemetryAlpha}") - implementation("io.opentelemetry:opentelemetry-exporter-jaeger-thrift:${versions.opentelemetry}") - implementation("io.opentelemetry.javaagent:opentelemetry-javaagent-spi:${versions.opentelemetryJavaagentAlpha}") - implementation("io.jaegertracing:jaeger-client:1.5.0") + compileOnly("io.opentelemetry:opentelemetry-sdk:${versions.opentelemetry}") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${versions.opentelemetryAlpha}") + compileOnly("io.opentelemetry:opentelemetry-semconv:${versions.opentelemetryAlpha}") + compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:${versions.opentelemetryJavaagentAlpha}") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-spi:${versions.opentelemetryJavaagentAlpha}") annotationProcessor("com.google.auto.service:auto-service:1.0-rc7") annotationProcessor("com.google.auto:auto-common:0.8") - implementation("com.google.auto.service:auto-service:1.0-rc7") - implementation("com.google.auto:auto-common:0.8") + compileOnly("com.google.auto.service:auto-service:1.0-rc7") + compileOnly("com.google.auto:auto-common:0.8") + + implementation("io.opentelemetry:opentelemetry-exporter-jaeger-thrift:${versions.opentelemetry}") + implementation("io.jaegertracing:jaeger-client:1.5.0") + + compileOnly("io.micrometer:micrometer-core:${versions.micrometer}") + implementation("io.micrometer:micrometer-registry-signalfx:${versions.micrometer}") { + // bootstrap already has micrometer-core + exclude(group: 'io.micrometer', module: 'micrometer-core') + } + + testImplementation("io.opentelemetry:opentelemetry-sdk:${versions.opentelemetry}") + testImplementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:${versions.opentelemetryJavaagentAlpha}") + testImplementation("io.micrometer:micrometer-core:${versions.micrometer}") } tasks { diff --git a/custom/src/main/java/com/splunk/opentelemetry/SplunkConfiguration.java b/custom/src/main/java/com/splunk/opentelemetry/SplunkConfiguration.java index ee45febc0..c7016839f 100644 --- a/custom/src/main/java/com/splunk/opentelemetry/SplunkConfiguration.java +++ b/custom/src/main/java/com/splunk/opentelemetry/SplunkConfiguration.java @@ -31,6 +31,8 @@ public Map getProperties() { // by default no metrics are exported config.put("otel.metrics.exporter", "none"); + // disable otel runtime-metrics instrumentation; we use micrometer metrics instead + config.put("otel.instrumentation.runtime-metrics.enabled", "false"); config.put("otel.traces.exporter", "jaeger-thrift-splunk"); // http://localhost:9080/v1/trace is the default endpoint for SmartAgent diff --git a/custom/src/main/java/com/splunk/opentelemetry/micrometer/GlobalTagsBuilder.java b/custom/src/main/java/com/splunk/opentelemetry/micrometer/GlobalTagsBuilder.java new file mode 100644 index 000000000..7d8e0e798 --- /dev/null +++ b/custom/src/main/java/com/splunk/opentelemetry/micrometer/GlobalTagsBuilder.java @@ -0,0 +1,48 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.micrometer; + +import io.micrometer.core.instrument.Tag; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.ArrayList; +import java.util.List; + +class GlobalTagsBuilder { + private final Resource resource; + + GlobalTagsBuilder(Resource resource) { + this.resource = resource; + } + + List build() { + List globalTags = new ArrayList<>(4); + addTag(globalTags, "deployment.environment", AttributeKey.stringKey("environment")); + addTag(globalTags, "service", ResourceAttributes.SERVICE_NAME); + addTag(globalTags, "runtime", ResourceAttributes.PROCESS_RUNTIME_NAME); + addTag(globalTags, "process.pid", ResourceAttributes.PROCESS_PID); + return globalTags; + } + + private void addTag(List tags, String tagName, AttributeKey resourceAttributeKey) { + Object value = resource.getAttributes().get(resourceAttributeKey); + if (value != null) { + tags.add(Tag.of(tagName, value.toString())); + } + } +} diff --git a/custom/src/main/java/com/splunk/opentelemetry/micrometer/MicrometerBootstrapPackagesProvider.java b/custom/src/main/java/com/splunk/opentelemetry/micrometer/MicrometerBootstrapPackagesProvider.java new file mode 100644 index 000000000..6d7a7bd36 --- /dev/null +++ b/custom/src/main/java/com/splunk/opentelemetry/micrometer/MicrometerBootstrapPackagesProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.micrometer; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.spi.BootstrapPackagesProvider; +import java.util.Arrays; +import java.util.List; + +@AutoService(BootstrapPackagesProvider.class) +public class MicrometerBootstrapPackagesProvider implements BootstrapPackagesProvider { + @Override + public List getPackagePrefixes() { + return Arrays.asList( + // IMPORTANT: must be io.micrometer.core, because io.micrometer.signalfx needs to be in the + // agent classloader + "io.micrometer.core", "org.HdrHistogram", "org.LatencyUtils"); + } +} diff --git a/custom/src/main/java/com/splunk/opentelemetry/micrometer/MicrometerInstaller.java b/custom/src/main/java/com/splunk/opentelemetry/micrometer/MicrometerInstaller.java new file mode 100644 index 000000000..01ad19662 --- /dev/null +++ b/custom/src/main/java/com/splunk/opentelemetry/micrometer/MicrometerInstaller.java @@ -0,0 +1,46 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.micrometer; + +import com.google.auto.service.AutoService; +import com.splunk.opentelemetry.javaagent.bootstrap.GlobalMetricsTags; +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.config.NamingConvention; +import io.micrometer.signalfx.SignalFxMeterRegistry; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.spi.ComponentInstaller; +import io.opentelemetry.sdk.autoconfigure.OpenTelemetrySdkAutoConfiguration; +import io.opentelemetry.sdk.resources.Resource; + +@AutoService(ComponentInstaller.class) +public class MicrometerInstaller implements ComponentInstaller { + @Override + public void beforeByteBuddyAgent() { + Resource resource = OpenTelemetrySdkAutoConfiguration.getResource(); + GlobalMetricsTags.set(new GlobalTagsBuilder(resource).build()); + Metrics.addRegistry(createSplunkMeterRegistry(resource)); + } + + private static SignalFxMeterRegistry createSplunkMeterRegistry(Resource resource) { + SignalFxMeterRegistry signalFxRegistry = + new SignalFxMeterRegistry(new SplunkMetricsConfig(Config.get(), resource), Clock.SYSTEM); + NamingConvention signalFxNamingConvention = signalFxRegistry.config().namingConvention(); + signalFxRegistry.config().namingConvention(new OtelNamingConvention(signalFxNamingConvention)); + return signalFxRegistry; + } +} diff --git a/custom/src/main/java/com/splunk/opentelemetry/micrometer/OtelNamingConvention.java b/custom/src/main/java/com/splunk/opentelemetry/micrometer/OtelNamingConvention.java new file mode 100644 index 000000000..8596b33af --- /dev/null +++ b/custom/src/main/java/com/splunk/opentelemetry/micrometer/OtelNamingConvention.java @@ -0,0 +1,58 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.micrometer; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.NamingConvention; + +/** + * This class adds {@code runtime.} prefix to all JVM metrics produced by micrometer (they all begin + * with {@code jvm.} by default). There are two reasons to do that: + * + *
    + *
  1. To match what OTel metrics spec says about runtime metrics: and pretty much the only thing + * that's specified there is that runtime metrics should start with {@code + * runtime.{environment}.} + *
  2. To avoid conflicts with the {@code opentelemetry-java-contrib/jmx-metrics} tool that starts + * all metrics with {@code jvm.} + *
+ */ +class OtelNamingConvention implements NamingConvention { + private final NamingConvention delegate; + + OtelNamingConvention(NamingConvention delegate) { + this.delegate = delegate; + } + + @Override + public String name(String name, Meter.Type type, String baseUnit) { + if (name.startsWith("jvm.")) { + name = "runtime." + name; + } + return delegate.name(name, type, baseUnit); + } + + @Override + public String tagKey(String key) { + return delegate.tagKey(key); + } + + @Override + public String tagValue(String value) { + return delegate.tagValue(value); + } +} diff --git a/custom/src/main/java/com/splunk/opentelemetry/micrometer/SplunkMetricsConfig.java b/custom/src/main/java/com/splunk/opentelemetry/micrometer/SplunkMetricsConfig.java new file mode 100644 index 000000000..f642f64ec --- /dev/null +++ b/custom/src/main/java/com/splunk/opentelemetry/micrometer/SplunkMetricsConfig.java @@ -0,0 +1,90 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.micrometer; + +import io.micrometer.signalfx.SignalFxConfig; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.time.Duration; + +class SplunkMetricsConfig implements SignalFxConfig { + static final String METRICS_ENABLED_PROPERTY = "splunk.metrics.enabled"; + static final String ACCESS_TOKEN_PROPERTY = "splunk.access.token"; + static final String METRICS_ENDPOINT_PROPERTY = "splunk.metrics.endpoint"; + static final String METRICS_EXPORT_INTERVAL_PROPERTY = "splunk.metrics.export.interval"; + + // right now the default value points to SmartAgent endpoint + static final String DEFAULT_METRICS_ENDPOINT = "http://localhost:9080/v2/datapoint"; + private static final String DEFAULT_METRICS_EXPORT_INTERVAL_MILLIS = "30000"; + + private final Config config; + // config values that are retrieved multiple times are cached + private final String accessToken; + private final String source; + private final Duration step; + + SplunkMetricsConfig(Config config, Resource resource) { + this.config = config; + + // non-empty token MUST be provided; we can just send anything because collector/SmartAgent will + // use the real one + accessToken = config.getProperty(ACCESS_TOKEN_PROPERTY, "no-token"); + source = resource.getAttributes().get(ResourceAttributes.SERVICE_NAME); + step = + Duration.ofMillis( + Long.parseLong( + config.getProperty( + METRICS_EXPORT_INTERVAL_PROPERTY, DEFAULT_METRICS_EXPORT_INTERVAL_MILLIS))); + } + + @Override + public boolean enabled() { + return config.getBooleanProperty(METRICS_ENABLED_PROPERTY, true); + } + + @Override + public String accessToken() { + return accessToken; + } + + @Override + public String uri() { + return config.getProperty(METRICS_ENDPOINT_PROPERTY, DEFAULT_METRICS_ENDPOINT); + } + + @Override + public String source() { + return source; + } + + @Override + public Duration step() { + return step; + } + + // hide other micrometer settings + @Override + public String prefix() { + return "splunk.internal.metrics"; + } + + @Override + public String get(String key) { + return config.getProperty(key); + } +} diff --git a/custom/src/test/java/com/splunk/opentelemetry/micrometer/GlobalTagsBuilderTest.java b/custom/src/test/java/com/splunk/opentelemetry/micrometer/GlobalTagsBuilderTest.java new file mode 100644 index 000000000..d8c5e7ad6 --- /dev/null +++ b/custom/src/test/java/com/splunk/opentelemetry/micrometer/GlobalTagsBuilderTest.java @@ -0,0 +1,67 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.micrometer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.micrometer.core.instrument.Tag; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Test; + +class GlobalTagsBuilderTest { + @Test + void shouldBuildEmptyTagsList() { + // given + var resource = Resource.create(Attributes.empty()); + + // when + var tags = new GlobalTagsBuilder(resource).build(); + + // then + assertTrue(tags.isEmpty()); + } + + @Test + void shouldBuildGlobalTagsList() { + // given + var resource = + Resource.create( + Attributes.of( + AttributeKey.stringKey("environment"), + "prod", + ResourceAttributes.SERVICE_NAME, + "my-service", + ResourceAttributes.PROCESS_RUNTIME_NAME, + "OpenJDK Runtime Environment", + ResourceAttributes.PROCESS_PID, + 12345L)); + + // when + var tags = new GlobalTagsBuilder(resource).build(); + + // then + assertEquals(4, tags.size()); + assertEquals(Tag.of("deployment.environment", "prod"), tags.get(0)); + assertEquals(Tag.of("service", "my-service"), tags.get(1)); + assertEquals(Tag.of("runtime", "OpenJDK Runtime Environment"), tags.get(2)); + assertEquals(Tag.of("process.pid", "12345"), tags.get(3)); + } +} diff --git a/custom/src/test/java/com/splunk/opentelemetry/micrometer/OtelNamingConventionTest.java b/custom/src/test/java/com/splunk/opentelemetry/micrometer/OtelNamingConventionTest.java new file mode 100644 index 000000000..68dbcf7fc --- /dev/null +++ b/custom/src/test/java/com/splunk/opentelemetry/micrometer/OtelNamingConventionTest.java @@ -0,0 +1,64 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.micrometer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.given; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.NamingConvention; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class OtelNamingConventionTest { + @Mock NamingConvention namingConventionMock; + + @Test + void shouldPrependJvmMetricsWithRuntime() { + // given + var finalMeterName = "sf_runtime.jvm.test.meter"; + given(namingConventionMock.name("runtime.jvm.test.meter", Meter.Type.OTHER, "unit")) + .willReturn(finalMeterName); + + var otelNamingConvention = new OtelNamingConvention(namingConventionMock); + + // when + var result = otelNamingConvention.name("jvm.test.meter", Meter.Type.OTHER, "unit"); + + // then + assertEquals(finalMeterName, result); + } + + @Test + void shouldNotModifyNonJvmMeterNames() { + // given + var finalMeterName = "sf_other.meter"; + given(namingConventionMock.name("other.meter", Meter.Type.OTHER, "unit")) + .willReturn(finalMeterName); + + var otelNamingConvention = new OtelNamingConvention(namingConventionMock); + + // when + var result = otelNamingConvention.name("other.meter", Meter.Type.OTHER, "unit"); + + // then + assertEquals(finalMeterName, result); + } +} diff --git a/custom/src/test/java/com/splunk/opentelemetry/micrometer/SplunkMetricsConfigTest.java b/custom/src/test/java/com/splunk/opentelemetry/micrometer/SplunkMetricsConfigTest.java new file mode 100644 index 000000000..e41de48bf --- /dev/null +++ b/custom/src/test/java/com/splunk/opentelemetry/micrometer/SplunkMetricsConfigTest.java @@ -0,0 +1,72 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.micrometer; + +import static com.splunk.opentelemetry.micrometer.SplunkMetricsConfig.ACCESS_TOKEN_PROPERTY; +import static com.splunk.opentelemetry.micrometer.SplunkMetricsConfig.DEFAULT_METRICS_ENDPOINT; +import static com.splunk.opentelemetry.micrometer.SplunkMetricsConfig.METRICS_ENABLED_PROPERTY; +import static com.splunk.opentelemetry.micrometer.SplunkMetricsConfig.METRICS_ENDPOINT_PROPERTY; +import static com.splunk.opentelemetry.micrometer.SplunkMetricsConfig.METRICS_EXPORT_INTERVAL_PROPERTY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class SplunkMetricsConfigTest { + @Test + void testDefaultValues() { + // given + var javaagentConfig = Config.create(Collections.emptyMap()); + var resource = Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "test-service")); + var splunkMetricsConfig = new SplunkMetricsConfig(javaagentConfig, resource); + + // when & then + assertTrue(splunkMetricsConfig.enabled()); + assertFalse(splunkMetricsConfig.accessToken().isBlank()); + assertEquals(DEFAULT_METRICS_ENDPOINT, splunkMetricsConfig.uri()); + assertEquals("test-service", splunkMetricsConfig.source()); + assertEquals(Duration.ofSeconds(30), splunkMetricsConfig.step()); + } + + @Test + void testCustomValues() { + var javaagentConfig = + Config.create( + Map.of( + METRICS_ENABLED_PROPERTY, "false", + ACCESS_TOKEN_PROPERTY, "token", + METRICS_ENDPOINT_PROPERTY, "http://my-endpoint:42", + METRICS_EXPORT_INTERVAL_PROPERTY, "60000")); + var resource = Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "test-service")); + var splunkMetricsConfig = new SplunkMetricsConfig(javaagentConfig, resource); + + // when & then + assertFalse(splunkMetricsConfig.enabled()); + assertEquals("token", splunkMetricsConfig.accessToken()); + assertEquals("http://my-endpoint:42", splunkMetricsConfig.uri()); + assertEquals("test-service", splunkMetricsConfig.source()); + assertEquals(Duration.ofSeconds(60), splunkMetricsConfig.step()); + } +} diff --git a/gradle/instrumentation.gradle b/gradle/instrumentation.gradle index 88cbf2a75..876ee7a6e 100644 --- a/gradle/instrumentation.gradle +++ b/gradle/instrumentation.gradle @@ -19,13 +19,14 @@ dependencies { compileOnly("net.bytebuddy:byte-buddy:1.10.10") annotationProcessor("com.google.auto.service:auto-service:1.0-rc3") annotationProcessor("com.google.auto:auto-common:0.8") - implementation("com.google.auto.service:auto-service:1.0-rc3") - implementation("com.google.auto:auto-common:0.8") + compileOnly("com.google.auto.service:auto-service:1.0-rc3") + compileOnly("com.google.auto:auto-common:0.8") compileOnly(project(":bootstrap")) + compileOnly("io.micrometer:micrometer-core:${versions.micrometer}") // test - testAgent("io.opentelemetry.javaagent:opentelemetry-agent-for-testing:${versions.opentelemetryJavaagentAlpha}") - testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common:${versions.opentelemetryJavaagentAlpha}") + testImplementation(project(":testing:common")) + testAgent(project(path: ":testing:agent-for-testing", configuration: "shadow")) } shadowJar { @@ -37,7 +38,12 @@ shadowJar { relocatePackages(it) } +evaluationDependsOn(":testing:agent-for-testing") + tasks.withType(Test).configureEach { + dependsOn shadowJar + dependsOn ":testing:agent-for-testing:shadowJar" + jvmArgs "-Dotel.javaagent.debug=true" jvmArgs "-javaagent:${configurations.testAgent.files.first().absolutePath}" jvmArgs "-Dotel.javaagent.experimental.initializer.jar=${shadowJar.archiveFile.get().asFile.absolutePath}" @@ -46,8 +52,6 @@ tasks.withType(Test).configureEach { // prevent sporadic gradle deadlocks, see SafeLogger for more details jvmArgs "-Dotel.javaagent.testing.transform-safe-logging.enabled=true" - dependsOn shadowJar - // The sources are packaged into the testing jar so we need to make sure to exclude from the test // classpath, which automatically inherits them, to ensure our shaded versions are used. classpath = classpath.filter { diff --git a/gradle/shadow.gradle b/gradle/shadow.gradle index 29c0e3b07..d1515f28e 100644 --- a/gradle/shadow.gradle +++ b/gradle/shadow.gradle @@ -18,4 +18,10 @@ ext.relocatePackages = { shadowJar -> // by the instrumentation modules that use them shadowJar.relocate "io.opentelemetry.extension.aws", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.aws" shadowJar.relocate "io.opentelemetry.extension.kotlin", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin" + + // relocate Micrometer + shadowJar.relocate("io.micrometer", "com.splunk.javaagent.shaded.io.micrometer") + // micrometer dependencies + shadowJar.relocate("org.HdrHistogram", "com.splunk.javaagent.shaded.org.hdrhistogram") + shadowJar.relocate("org.LatencyUtils", "com.splunk.javaagent.shaded.org.latencyutils") } diff --git a/instrumentation/jvm-metrics/build.gradle b/instrumentation/jvm-metrics/build.gradle new file mode 100644 index 000000000..80b3cc1b1 --- /dev/null +++ b/instrumentation/jvm-metrics/build.gradle @@ -0,0 +1 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" diff --git a/instrumentation/jvm-metrics/src/main/java/com/splunk/opentelemetry/jvmmetrics/JvmMetricsInstaller.java b/instrumentation/jvm-metrics/src/main/java/com/splunk/opentelemetry/jvmmetrics/JvmMetricsInstaller.java new file mode 100644 index 000000000..07682efcf --- /dev/null +++ b/instrumentation/jvm-metrics/src/main/java/com/splunk/opentelemetry/jvmmetrics/JvmMetricsInstaller.java @@ -0,0 +1,50 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.jvmmetrics; + +import static java.util.Collections.singleton; + +import com.google.auto.service.AutoService; +import com.splunk.opentelemetry.javaagent.bootstrap.GlobalMetricsTags; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.spi.ComponentInstaller; +import java.util.List; + +@AutoService(ComponentInstaller.class) +public class JvmMetricsInstaller implements ComponentInstaller { + @Override + public void afterByteBuddyAgent() { + if (!Config.get() + .isInstrumentationEnabled( + singleton("jvm-metrics"), + Config.get().getBooleanProperty("splunk.metrics.enabled", true))) { + return; + } + + List tags = GlobalMetricsTags.get(); + new ClassLoaderMetrics(tags).bindTo(Metrics.globalRegistry); + new JvmGcMetrics(tags).bindTo(Metrics.globalRegistry); + new JvmMemoryMetrics(tags).bindTo(Metrics.globalRegistry); + new JvmThreadMetrics(tags).bindTo(Metrics.globalRegistry); + } +} diff --git a/instrumentation/jvm-metrics/src/test/java/com/splunk/opentelemetry/jvmmetrics/JvmMetricsTest.java b/instrumentation/jvm-metrics/src/test/java/com/splunk/opentelemetry/jvmmetrics/JvmMetricsTest.java new file mode 100644 index 000000000..047430e12 --- /dev/null +++ b/instrumentation/jvm-metrics/src/test/java/com/splunk/opentelemetry/jvmmetrics/JvmMetricsTest.java @@ -0,0 +1,41 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.jvmmetrics; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.splunk.opentelemetry.testing.TestMetricsAccess; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(AgentInstrumentationExtension.class) +class JvmMetricsTest { + @Test + void shouldRegisterJvmMeters() { + var meterNames = TestMetricsAccess.getMeterNames(); + + // classloader metrics + assertTrue(meterNames.contains("jvm.classes.loaded")); + // GC metrics + assertTrue(meterNames.contains("jvm.gc.memory.allocated")); + // memory metrics + assertTrue(meterNames.contains("jvm.memory.used")); + // thread metrics + assertTrue(meterNames.contains("jvm.threads.peak")); + } +} diff --git a/settings.gradle b/settings.gradle index e4051013d..460f532f2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,6 +17,7 @@ include("agent", "instrumentation:compile-stub", "instrumentation:glassfish", "instrumentation:jetty", + "instrumentation:jvm-metrics", "instrumentation:liberty", "instrumentation:netty-3.8", "instrumentation:netty-4.0", @@ -26,5 +27,8 @@ include("agent", "instrumentation:tomee", "instrumentation:weblogic", "instrumentation:wildfly", + "matrix", "smoke-tests", - "matrix") \ No newline at end of file + "testing:agent-for-testing", + "testing:agent-metrics", + "testing:common") \ No newline at end of file diff --git a/smoke-tests/src/test/java/com/splunk/opentelemetry/WildFlySmokeTest.java b/smoke-tests/src/test/java/com/splunk/opentelemetry/WildFlySmokeTest.java index 9aec9c17b..c94745772 100644 --- a/smoke-tests/src/test/java/com/splunk/opentelemetry/WildFlySmokeTest.java +++ b/smoke-tests/src/test/java/com/splunk/opentelemetry/WildFlySmokeTest.java @@ -18,6 +18,7 @@ import com.splunk.opentelemetry.helper.TestImage; import java.io.IOException; +import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -42,6 +43,17 @@ private static Stream supportedConfigurations() { .splunkWindows("21.0.0.Final", WILDFLY_21_SERVER_ATTRIBUTES, VMS_ALL, "8", "11").stream(); } + // openj9 JDK8 does not have JFR classes, which causes all ComponentInstallers to run + // synchronously, not after LogManager is loaded - see + // AgentInstaller#installComponentsAfterByteBuddy() + // micrometer's JvmGcMetrics references (indirectly) NotificationBroadcasterSupport which uses + // ClassLogger which uses JUL and this causes JVM LogManager to load before the JBoss one + // AFAIK only openj9 (IBM) JDK 8 has this problem, all other JDKs don't use JUL in MBeans + @Override + protected Map getExtraEnv() { + return Map.of("OTEL_INSTRUMENTATION_JVM_METRICS_ENABLED", "false"); + } + @ParameterizedTest(name = "[{index}] {0}") @MethodSource("supportedConfigurations") void wildflySmokeTest(TestImage image, ExpectedServerAttributes expectedServerAttributes) diff --git a/testing/agent-for-testing/build.gradle b/testing/agent-for-testing/build.gradle new file mode 100644 index 000000000..8e704b363 --- /dev/null +++ b/testing/agent-for-testing/build.gradle @@ -0,0 +1,81 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id("com.github.johnrengelman.shadow") version "6.0.0" +} + +apply from: "$rootDir/gradle/shadow.gradle" + +def versions = ext.versions +def relocatePackages = ext.relocatePackages + +configurations { + // dependencies that already are relocated and will be moved to inst/ (agent classloader isolation) + isolateLibs + // dependencies that will be relocated + relocateLibs + // dependencies that will be included as they are + includeAsIs +} + +dependencies { + // include micrometer-core API + relocateLibs(project(":bootstrap")) + // include testing micrometer MetricsRegistry + isolateLibs(project(path: ":testing:agent-metrics", configuration: "shadow")) + + // and finally include everything from otel agent for testing + includeAsIs("io.opentelemetry.javaagent:opentelemetry-agent-for-testing:${versions.opentelemetryJavaagentAlpha}") +} + +CopySpec isolateAgentClasses(Iterable jars) { + return copySpec { + jars.each { + from(zipTree(it)) { + into("inst") + rename("(^.*)\\.class\$", "\$1.classdata") + } + } + } +} + +task relocateAndIsolate(type: ShadowJar) { + dependsOn ':testing:agent-metrics:shadowJar' + + configurations = [project.configurations.relocateLibs] + + with isolateAgentClasses(project.configurations.isolateLibs.files) + + mergeServiceFiles() + exclude("**/module-info.class") + + relocatePackages(it) +} + +shadowJar { + from project.configurations.includeAsIs.files + from tasks.relocateAndIsolate.outputs.files + + manifest { + attributes( + "Main-Class": "io.opentelemetry.javaagent.OpenTelemetryAgent", + "Agent-Class": "io.opentelemetry.javaagent.OpenTelemetryAgent", + "Premain-Class": "io.opentelemetry.javaagent.OpenTelemetryAgent", + "Can-Redefine-Classes": true, + "Can-Retransform-Classes": true, + ) + } + + mergeServiceFiles { + include("inst/META-INF/services/**") + } + exclude("**/module-info.class") + + relocatePackages(it) +} + +jar { + enabled = false +} + +tasks.assemble.dependsOn(tasks.shadowJar) \ No newline at end of file diff --git a/testing/agent-metrics/build.gradle b/testing/agent-metrics/build.gradle new file mode 100644 index 000000000..a16327d06 --- /dev/null +++ b/testing/agent-metrics/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id "com.github.johnrengelman.shadow" version "6.0.0" +} + +apply from: "$rootDir/gradle/shadow.gradle" + +def versions = ext.versions +def relocatePackages = ext.relocatePackages + +dependencies { + annotationProcessor("com.google.auto.service:auto-service:1.0-rc7") + annotationProcessor("com.google.auto:auto-common:0.8") + compileOnly("com.google.auto.service:auto-service:1.0-rc7") + compileOnly("com.google.auto:auto-common:0.8") + + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-spi:${versions.opentelemetryJavaagentAlpha}") + compileOnly("io.micrometer:micrometer-core:${versions.micrometer}") +} + +tasks { + compileJava { + options.release.set(8) + } + + shadowJar { + mergeServiceFiles() + + exclude("**/module-info.class") + + relocatePackages(it) + } +} diff --git a/testing/agent-metrics/src/main/java/com/splunk/opentelemetry/testing/TestMetrics.java b/testing/agent-metrics/src/main/java/com/splunk/opentelemetry/testing/TestMetrics.java new file mode 100644 index 000000000..3d07de44d --- /dev/null +++ b/testing/agent-metrics/src/main/java/com/splunk/opentelemetry/testing/TestMetrics.java @@ -0,0 +1,33 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.testing; + +import io.micrometer.core.instrument.Metrics; +import java.util.Set; +import java.util.stream.Collectors; + +/** This class is used in instrumentation tests; accessed via agent classloader bridging. */ +@SuppressWarnings("unused") +public final class TestMetrics { + public static Set getMeterNames() { + return Metrics.globalRegistry.getMeters().stream() + .map(meter -> meter.getId().getName()) + .collect(Collectors.toSet()); + } + + private TestMetrics() {} +} diff --git a/testing/agent-metrics/src/main/java/com/splunk/opentelemetry/testing/TestMicrometerInstaller.java b/testing/agent-metrics/src/main/java/com/splunk/opentelemetry/testing/TestMicrometerInstaller.java new file mode 100644 index 000000000..5f0a5cdab --- /dev/null +++ b/testing/agent-metrics/src/main/java/com/splunk/opentelemetry/testing/TestMicrometerInstaller.java @@ -0,0 +1,30 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.testing; + +import com.google.auto.service.AutoService; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.opentelemetry.javaagent.spi.ComponentInstaller; + +@AutoService(ComponentInstaller.class) +public class TestMicrometerInstaller implements ComponentInstaller { + @Override + public void beforeByteBuddyAgent() { + Metrics.addRegistry(new SimpleMeterRegistry()); + } +} diff --git a/testing/common/build.gradle b/testing/common/build.gradle new file mode 100644 index 000000000..e80a476a2 --- /dev/null +++ b/testing/common/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java-library' +} + +def versions = ext.versions + +dependencies { + api("io.opentelemetry.javaagent:opentelemetry-testing-common:${versions.opentelemetryJavaagentAlpha}") +} diff --git a/testing/common/src/main/java/com/splunk/opentelemetry/testing/TestMetricsAccess.java b/testing/common/src/main/java/com/splunk/opentelemetry/testing/TestMetricsAccess.java new file mode 100644 index 000000000..e17e9e5af --- /dev/null +++ b/testing/common/src/main/java/com/splunk/opentelemetry/testing/TestMetricsAccess.java @@ -0,0 +1,51 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.testing; + +import io.opentelemetry.javaagent.testing.common.AgentClassLoaderAccess; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Set; + +public final class TestMetricsAccess { + private static final MethodHandle getMeterNames; + + static { + try { + Class testMetricsClass = + AgentClassLoaderAccess.getAgentClassLoader() + .loadClass("com.splunk.opentelemetry.testing.TestMetrics"); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + getMeterNames = + lookup.findStatic(testMetricsClass, "getMeterNames", MethodType.methodType(Set.class)); + } catch (Exception e) { + throw new Error("Error accessing fields with reflection.", e); + } + } + + @SuppressWarnings("unchecked") + public static Set getMeterNames() { + try { + return (Set) getMeterNames.invokeExact(); + } catch (Throwable throwable) { + throw new AssertionError("Could not invoke getMeterNames", throwable); + } + } + + private TestMetricsAccess() {} +}