diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/ContractTestBase.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/ContractTestBase.java index 16bd1af119..6b4b0298ef 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/ContractTestBase.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/ContractTestBase.java @@ -79,6 +79,7 @@ public abstract class ContractTestBase { .withEnv("JAVA_TOOL_OPTIONS", "-javaagent:/opentelemetry-javaagent-all.jar") .withEnv("OTEL_METRIC_EXPORT_INTERVAL", "100") // 100 ms .withEnv("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", "true") + .withEnv("OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED", "false") .withEnv("OTEL_METRICS_EXPORTER", "none") .withEnv("OTEL_BSP_SCHEDULE_DELAY", "0") // Don't wait to export spans to the collector .withEnv( diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java index 2d16135352..bdf12fadb2 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java @@ -15,6 +15,8 @@ package software.amazon.opentelemetry.javaagent.providers; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.contrib.awsxray.AlwaysRecordSampler; import io.opentelemetry.contrib.awsxray.ResourceHolder; @@ -25,18 +27,29 @@ import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentSelector; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.View; 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 io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -61,19 +74,28 @@ public class AwsApplicationSignalsCustomizerProvider private static final Logger logger = Logger.getLogger(AwsApplicationSignalsCustomizerProvider.class.getName()); - private static final String SMP_ENABLED_CONFIG = "otel.smp.enabled"; - private static final String APP_SIGNALS_ENABLED_CONFIG = "otel.aws.app.signals.enabled"; + private static final String DEPRECATED_SMP_ENABLED_CONFIG = "otel.smp.enabled"; + private static final String DEPRECATED_APP_SIGNALS_ENABLED_CONFIG = + "otel.aws.app.signals.enabled"; private static final String APPLICATION_SIGNALS_ENABLED_CONFIG = "otel.aws.application.signals.enabled"; - private static final String SMP_EXPORTER_ENDPOINT_CONFIG = "otel.aws.smp.exporter.endpoint"; - private static final String APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = + private static final String APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG = + "otel.aws.application.signals.runtime.enabled"; + private static final String DEPRECATED_SMP_EXPORTER_ENDPOINT_CONFIG = + "otel.aws.smp.exporter.endpoint"; + private static final String DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "otel.aws.app.signals.exporter.endpoint"; private static final String APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "otel.aws.application.signals.exporter.endpoint"; + private static final String OTEL_JMX_TARGET_SYSTEM_CONFIG = "otel.jmx.target.system"; + public void customize(AutoConfigurationCustomizer autoConfiguration) { + autoConfiguration.addPropertiesCustomizer(this::customizeProperties); + autoConfiguration.addResourceCustomizer(this::customizeResource); autoConfiguration.addSamplerCustomizer(this::customizeSampler); autoConfiguration.addTracerProviderCustomizer(this::customizeTracerProviderBuilder); + autoConfiguration.addMeterProviderCustomizer(this::customizeMeterProvider); autoConfiguration.addSpanExporterCustomizer(this::customizeSpanExporter); } @@ -81,7 +103,44 @@ private boolean isApplicationSignalsEnabled(ConfigProperties configProps) { return configProps.getBoolean( APPLICATION_SIGNALS_ENABLED_CONFIG, configProps.getBoolean( - APP_SIGNALS_ENABLED_CONFIG, configProps.getBoolean(SMP_ENABLED_CONFIG, false))); + DEPRECATED_APP_SIGNALS_ENABLED_CONFIG, + configProps.getBoolean(DEPRECATED_SMP_ENABLED_CONFIG, false))); + } + + private boolean isApplicationSignalsRuntimeEnabled(ConfigProperties configProps) { + return isApplicationSignalsEnabled(configProps) + && configProps.getBoolean(APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG, true); + } + + private Map customizeProperties(ConfigProperties configProps) { + if (isApplicationSignalsRuntimeEnabled(configProps)) { + List list = configProps.getList(OTEL_JMX_TARGET_SYSTEM_CONFIG); + if (list.contains("jvm")) { + logger.log(Level.INFO, "Found jmx in {0}", OTEL_JMX_TARGET_SYSTEM_CONFIG); + return Collections.emptyMap(); + } else { + logger.log(Level.INFO, "Configure jmx in {0}", OTEL_JMX_TARGET_SYSTEM_CONFIG); + List jmxTargets = new ArrayList<>(list); + jmxTargets.add("jvm"); + Map propsOverride = new HashMap<>(1); + propsOverride.put(OTEL_JMX_TARGET_SYSTEM_CONFIG, String.join(",", jmxTargets)); + return propsOverride; + } + } + return Collections.emptyMap(); + } + + private Resource customizeResource(Resource resource, ConfigProperties configProps) { + if (isApplicationSignalsEnabled(configProps)) { + AttributesBuilder builder = Attributes.builder(); + AwsResourceAttributeConfigurator.setServiceAttribute( + resource, + builder, + () -> logger.log(Level.WARNING, "Service name is undefined, use UnknownService instead")); + Resource additionalResource = Resource.create((builder.build())); + return resource.merge(additionalResource); + } + return resource; } private Sampler customizeSampler(Sampler sampler, ConfigProperties configProps) { @@ -95,20 +154,7 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder( SdkTracerProviderBuilder tracerProviderBuilder, ConfigProperties configProps) { if (isApplicationSignalsEnabled(configProps)) { logger.info("AWS Application Signals enabled"); - Duration exportInterval = - configProps.getDuration("otel.metric.export.interval", DEFAULT_METRIC_EXPORT_INTERVAL); - logger.log( - Level.FINE, - String.format("AWS Application Signals Metrics export interval: %s", exportInterval)); - // Cap export interval to 60 seconds. This is currently required for metrics-trace correlation - // to work correctly. - if (exportInterval.compareTo(DEFAULT_METRIC_EXPORT_INTERVAL) > 0) { - exportInterval = DEFAULT_METRIC_EXPORT_INTERVAL; - logger.log( - Level.INFO, - String.format( - "AWS Application Signals metrics export interval capped to %s", exportInterval)); - } + Duration exportInterval = getMetricExportInterval(configProps); // Construct and set local and remote attributes span processor tracerProviderBuilder.addSpanProcessor( AttributePropagatingSpanProcessorBuilder.create().build()); @@ -133,6 +179,67 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder( return tracerProviderBuilder; } + private SdkMeterProviderBuilder customizeMeterProvider( + SdkMeterProviderBuilder sdkMeterProviderBuilder, ConfigProperties configProps) { + + if (isApplicationSignalsRuntimeEnabled(configProps)) { + Set registeredScopeNames = new HashSet<>(1); + String jmxRuntimeScopeName = "io.opentelemetry.jmx"; + registeredScopeNames.add(jmxRuntimeScopeName); + + configureMetricFilter(configProps, sdkMeterProviderBuilder, registeredScopeNames); + + MetricExporter metricsExporter = + ApplicationSignalsExporterProvider.INSTANCE.createExporter(configProps); + MetricReader metricReader = + ScopeBasedPeriodicMetricReader.create(metricsExporter, registeredScopeNames) + .setInterval(getMetricExportInterval(configProps)) + .build(); + sdkMeterProviderBuilder.registerMetricReader(metricReader); + + logger.info("AWS Application Signals runtime metric collection enabled"); + } + return sdkMeterProviderBuilder; + } + + private static void configureMetricFilter( + ConfigProperties configProps, + SdkMeterProviderBuilder sdkMeterProviderBuilder, + Set registeredScopeNames) { + Set exporterNames = + DefaultConfigProperties.getSet(configProps, "otel.metrics.exporter"); + if (exporterNames.contains("none")) { + for (String scope : registeredScopeNames) { + sdkMeterProviderBuilder.registerView( + InstrumentSelector.builder().setMeterName(scope).build(), + View.builder().setAggregation(Aggregation.defaultAggregation()).build()); + + logger.log(Level.FINE, "Registered scope {0}", scope); + } + sdkMeterProviderBuilder.registerView( + InstrumentSelector.builder().setName("*").build(), + View.builder().setAggregation(Aggregation.drop()).build()); + } + } + + private static Duration getMetricExportInterval(ConfigProperties configProps) { + Duration exportInterval = + configProps.getDuration("otel.metric.export.interval", DEFAULT_METRIC_EXPORT_INTERVAL); + logger.log( + Level.FINE, + String.format("AWS Application Signals Metrics export interval: %s", exportInterval)); + // Cap export interval to 60 seconds. This is currently required for metrics-trace correlation + // to work correctly. + if (exportInterval.compareTo(DEFAULT_METRIC_EXPORT_INTERVAL) > 0) { + exportInterval = DEFAULT_METRIC_EXPORT_INTERVAL; + logger.log( + Level.INFO, + String.format( + "AWS Application Signals metrics export interval capped to %s", exportInterval)); + } + return exportInterval; + } + private SpanExporter customizeSpanExporter( SpanExporter spanExporter, ConfigProperties configProps) { if (isApplicationSignalsEnabled(configProps)) { @@ -159,9 +266,10 @@ public MetricExporter createExporter(ConfigProperties configProps) { configProps.getString( APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG, configProps.getString( - APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, + DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, configProps.getString( - SMP_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4316/v1/metrics"))); + DEPRECATED_SMP_EXPORTER_ENDPOINT_CONFIG, + "http://localhost:4316/v1/metrics"))); logger.log( Level.FINE, String.format( @@ -169,15 +277,16 @@ public MetricExporter createExporter(ConfigProperties configProps) { return OtlpHttpMetricExporter.builder() .setEndpoint(applicationSignalsEndpoint) .setDefaultAggregationSelector(this::getAggregation) - .setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred()) + .setAggregationTemporalitySelector(CloudWatchTemporalitySelector.alwaysDelta()) .build(); } else if (protocol.equals(OtlpConfigUtil.PROTOCOL_GRPC)) { applicationSignalsEndpoint = configProps.getString( APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG, configProps.getString( - APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, - configProps.getString(SMP_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4315"))); + DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, + configProps.getString( + DEPRECATED_SMP_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4315"))); logger.log( Level.FINE, String.format( @@ -185,7 +294,7 @@ public MetricExporter createExporter(ConfigProperties configProps) { return OtlpGrpcMetricExporter.builder() .setEndpoint(applicationSignalsEndpoint) .setDefaultAggregationSelector(this::getAggregation) - .setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred()) + .setAggregationTemporalitySelector(CloudWatchTemporalitySelector.alwaysDelta()) .build(); } throw new ConfigurationException( diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java index c8c39dff91..f612435032 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java @@ -15,7 +15,6 @@ package software.amazon.opentelemetry.javaagent.providers; -import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_NAME; import static io.opentelemetry.semconv.SemanticAttributes.DB_CONNECTION_STRING; import static io.opentelemetry.semconv.SemanticAttributes.DB_NAME; import static io.opentelemetry.semconv.SemanticAttributes.DB_OPERATION; @@ -64,7 +63,6 @@ import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.UNKNOWN_OPERATION; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.UNKNOWN_REMOTE_OPERATION; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.UNKNOWN_REMOTE_SERVICE; -import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.UNKNOWN_SERVICE; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.isAwsSDKSpan; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.isDBSpan; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.isKeyPresent; @@ -119,11 +117,6 @@ final class AwsMetricAttributeGenerator implements MetricAttributeGenerator { private static final String DB_CONNECTION_RESOURCE_TYPE = "DB::Connection"; - // As per - // https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure#opentelemetry-resource - // If service name is not specified, SDK defaults the service name to unknown_service:java - private static final String OTEL_UNKNOWN_SERVICE = "unknown_service:java"; - // This method is used by the AwsSpanMetricsProcessor to generate service and dependency metrics @Override public Map generateMetricAttributeMapFromSpan( @@ -167,14 +160,8 @@ private Attributes generateDependencyMetricAttributes(SpanData span, Resource re /** Service is always derived from {@link ResourceAttributes#SERVICE_NAME} */ private static void setService(Resource resource, SpanData span, AttributesBuilder builder) { - String service = resource.getAttribute(SERVICE_NAME); - - // In practice the service name is never null, but we can be defensive here. - if (service == null || service.equals(OTEL_UNKNOWN_SERVICE)) { - logUnknownAttribute(AWS_LOCAL_SERVICE, span); - service = UNKNOWN_SERVICE; - } - builder.put(AWS_LOCAL_SERVICE, service); + AwsResourceAttributeConfigurator.setServiceAttribute( + resource, builder, () -> logUnknownAttribute(AWS_LOCAL_SERVICE, span)); } /** diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsResourceAttributeConfigurator.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsResourceAttributeConfigurator.java new file mode 100644 index 0000000000..d2decdc16c --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsResourceAttributeConfigurator.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.opentelemetry.javaagent.providers; + +import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_NAME; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_SERVICE; +import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.UNKNOWN_SERVICE; + +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; + +public class AwsResourceAttributeConfigurator { + // As per + // https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure#opentelemetry-resource + // If service name is not specified, SDK defaults the service name to unknown_service:java + private static final String OTEL_UNKNOWN_SERVICE = "unknown_service:java"; + + public static void setServiceAttribute( + Resource resource, AttributesBuilder attributesBuilder, Runnable handleUnknownService) { + String service = resource.getAttribute(AWS_LOCAL_SERVICE); + if (service == null) { + service = resource.getAttribute(SERVICE_NAME); + // In practice the service name is never null, but we can be defensive here. + if (service == null || service.equals(OTEL_UNKNOWN_SERVICE)) { + service = UNKNOWN_SERVICE; + handleUnknownService.run(); + } + } + attributesBuilder.put(AWS_LOCAL_SERVICE, service); + } +} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/CloudWatchTemporalitySelector.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/CloudWatchTemporalitySelector.java new file mode 100644 index 0000000000..36b0052061 --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/CloudWatchTemporalitySelector.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.opentelemetry.javaagent.providers; + +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; + +public class CloudWatchTemporalitySelector { + static AggregationTemporalitySelector alwaysDelta() { + return (instrumentType) -> { + return AggregationTemporality.DELTA; + }; + } +} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/ScopeBasedPeriodicMetricReader.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/ScopeBasedPeriodicMetricReader.java new file mode 100644 index 0000000000..792a02fb5c --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/ScopeBasedPeriodicMetricReader.java @@ -0,0 +1,241 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.opentelemetry.javaagent.providers; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.export.MemoryMode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.CollectionRegistration; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * The {@code ScopeBasedPeriodicMetricReader} class is a customized implementation to extend the + * functionality of the {@link io.opentelemetry.sdk.metrics.export.PeriodicMetricReader}. Due to the + * fact that {@link io.opentelemetry.sdk.metrics.export.PeriodicMetricReader} is a final class and + * cannot be directly extended, this class duplicates and modifies the relevant code to support + * scope-based metric reading. + * + *

Source code based on opentelemetry-java v1.34.1. + */ +public class ScopeBasedPeriodicMetricReader implements MetricReader { + private static final Logger logger = + Logger.getLogger(ScopeBasedPeriodicMetricReader.class.getName()); + + private final MetricExporter exporter; + private final long intervalNanos; + private final ScheduledExecutorService scheduler; + private final Scheduled scheduled; + private final Object lock = new Object(); + private volatile CollectionRegistration collectionRegistration = CollectionRegistration.noop(); + + @Nullable private volatile ScheduledFuture scheduledFuture; + + public static ScopeBasedPeriodicMetricReaderBuilder create( + MetricExporter exporter, Set registeredScopeNames) { + return new ScopeBasedPeriodicMetricReaderBuilder(exporter, registeredScopeNames); + } + + ScopeBasedPeriodicMetricReader( + MetricExporter exporter, + long intervalNanos, + ScheduledExecutorService scheduler, + Set registeredScopeNames) { + this.exporter = exporter; + this.intervalNanos = intervalNanos; + this.scheduler = scheduler; + this.scheduled = new Scheduled(registeredScopeNames); + } + + /** + * This method is a direct copy from the{@link + * io.opentelemetry.sdk.metrics.export.PeriodicMetricReader} class and has not been modified. + */ + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return exporter.getAggregationTemporality(instrumentType); + } + + /** + * This method is a direct copy from the{@link + * io.opentelemetry.sdk.metrics.export.PeriodicMetricReader} class and has not been modified. + */ + @Override + public Aggregation getDefaultAggregation(InstrumentType instrumentType) { + return exporter.getDefaultAggregation(instrumentType); + } + + /** + * This method is a direct copy from the{@link + * io.opentelemetry.sdk.metrics.export.PeriodicMetricReader} class and has not been modified. + */ + @Override + public MemoryMode getMemoryMode() { + return exporter.getMemoryMode(); + } + + /** + * This method is a direct copy from the{@link + * io.opentelemetry.sdk.metrics.export.PeriodicMetricReader} class and has not been modified. + */ + @Override + public CompletableResultCode forceFlush() { + return scheduled.doRun(); + } + + /** + * This method is a direct copy from the{@link + * io.opentelemetry.sdk.metrics.export.PeriodicMetricReader} class and has not been modified. + */ + @Override + public CompletableResultCode shutdown() { + CompletableResultCode result = new CompletableResultCode(); + ScheduledFuture scheduledFuture = this.scheduledFuture; + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + } + scheduler.shutdown(); + try { + scheduler.awaitTermination(5, TimeUnit.SECONDS); + CompletableResultCode flushResult = scheduled.doRun(); + flushResult.join(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // force a shutdown if the export hasn't finished. + scheduler.shutdownNow(); + // reset the interrupted status + Thread.currentThread().interrupt(); + } finally { + CompletableResultCode shutdownResult = scheduled.shutdown(); + shutdownResult.whenComplete( + () -> { + if (!shutdownResult.isSuccess()) { + result.fail(); + } else { + result.succeed(); + } + }); + } + return result; + } + + /** + * This method is a direct copy from the{@link + * io.opentelemetry.sdk.metrics.export.PeriodicMetricReader} class and has not been modified. + */ + @Override + public void register(CollectionRegistration collectionRegistration) { + this.collectionRegistration = collectionRegistration; + start(); + } + + @Override + public String toString() { + return "ScopeBasedPeriodicMetricReader{" + + "exporter=" + + exporter + + ", intervalNanos=" + + intervalNanos + + '}'; + } + + /** + * This method is a direct copy from the{@link + * io.opentelemetry.sdk.metrics.export.PeriodicMetricReader} class and has not been modified. + */ + void start() { + synchronized (lock) { + if (scheduledFuture != null) { + return; + } + scheduledFuture = + scheduler.scheduleAtFixedRate( + scheduled, intervalNanos, intervalNanos, TimeUnit.NANOSECONDS); + } + } + + private final class Scheduled implements Runnable { + private final AtomicBoolean exportAvailable = new AtomicBoolean(true); + private final Set registeredScopeNames; + + private Scheduled(Set registeredScopeNames) { + this.registeredScopeNames = registeredScopeNames; + } + + @Override + public void run() { + // Ignore the CompletableResultCode from doRun() in order to keep run() asynchronous + doRun(); + } + + // Runs a collect + export cycle. + CompletableResultCode doRun() { + CompletableResultCode flushResult = new CompletableResultCode(); + if (exportAvailable.compareAndSet(true, false)) { + try { + Collection metricData = collectionRegistration.collectAllMetrics(); + if (metricData.isEmpty()) { + logger.log(Level.FINE, "No metric data to export - skipping export."); + flushResult.succeed(); + exportAvailable.set(true); + } else { + List exportingMetrics = new LinkedList<>(); + for (MetricData metricDatum : metricData) { + String scopeName = metricDatum.getInstrumentationScopeInfo().getName(); + if (registeredScopeNames.contains(scopeName)) { + exportingMetrics.add(metricDatum); + } + } + CompletableResultCode result = exporter.export(exportingMetrics); + result.whenComplete( + () -> { + if (!result.isSuccess()) { + logger.log(Level.FINE, "Exporter failed"); + } + flushResult.succeed(); + exportAvailable.set(true); + }); + } + } catch (Throwable t) { + exportAvailable.set(true); + logger.log(Level.WARNING, "Exporter threw an Exception", t); + flushResult.fail(); + } + } else { + logger.log(Level.FINE, "Exporter busy. Dropping metrics."); + flushResult.fail(); + } + return flushResult; + } + + CompletableResultCode shutdown() { + return exporter.shutdown(); + } + } +} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/ScopeBasedPeriodicMetricReaderBuilder.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/ScopeBasedPeriodicMetricReaderBuilder.java new file mode 100644 index 0000000000..fea1133890 --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/ScopeBasedPeriodicMetricReaderBuilder.java @@ -0,0 +1,80 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.opentelemetry.javaagent.providers; + +import static io.opentelemetry.api.internal.Utils.checkArgument; +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.sdk.internal.DaemonThreadFactory; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +public class ScopeBasedPeriodicMetricReaderBuilder { + static final long DEFAULT_SCHEDULE_DELAY_MINUTES = 1; + private final MetricExporter metricExporter; + private final Set registeredScopeNames; + private long intervalNanos = TimeUnit.MINUTES.toNanos(DEFAULT_SCHEDULE_DELAY_MINUTES); + + @Nullable private ScheduledExecutorService executor; + + public ScopeBasedPeriodicMetricReaderBuilder( + MetricExporter metricExporter, Set registeredScopeNames) { + this.metricExporter = metricExporter; + this.registeredScopeNames = registeredScopeNames; + } + + /** + * Sets the interval of reads. If unset, defaults to {@value DEFAULT_SCHEDULE_DELAY_MINUTES}min. + */ + public ScopeBasedPeriodicMetricReaderBuilder setInterval(long interval, TimeUnit unit) { + requireNonNull(unit, "unit"); + checkArgument(interval > 0, "interval must be positive"); + intervalNanos = unit.toNanos(interval); + return this; + } + + /** + * Sets the interval of reads. If unset, defaults to {@value DEFAULT_SCHEDULE_DELAY_MINUTES}min. + */ + public ScopeBasedPeriodicMetricReaderBuilder setInterval(Duration interval) { + requireNonNull(interval, "interval"); + return setInterval(interval.toNanos(), TimeUnit.NANOSECONDS); + } + + /** Sets the {@link ScheduledExecutorService} to schedule reads on. */ + public ScopeBasedPeriodicMetricReaderBuilder setExecutor(ScheduledExecutorService executor) { + requireNonNull(executor, "executor"); + this.executor = executor; + return this; + } + + /** Build a {@link ScopeBasedPeriodicMetricReader} with the configuration of this builder. */ + public ScopeBasedPeriodicMetricReader build() { + ScheduledExecutorService executor = this.executor; + if (executor == null) { + executor = + Executors.newScheduledThreadPool( + 1, new DaemonThreadFactory("AwsScopeBasedPeriodicMetricReader")); + } + return new ScopeBasedPeriodicMetricReader( + metricExporter, intervalNanos, executor, registeredScopeNames); + } +} diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsTracerConfigurerTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsTracerConfigurerTest.java index 289a35e33e..bd91c90191 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsTracerConfigurerTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsTracerConfigurerTest.java @@ -39,6 +39,8 @@ private AutoConfiguredOpenTelemetrySdkBuilder createSdkBuilder() { return tracerProviderBuilder.addSpanProcessor( SimpleSpanProcessor.create(spanExporter)); }) + .addPropertiesSupplier( + () -> singletonMap("otel.aws.application.signals.runtime.enabled", "false")) .addPropertiesSupplier(() -> singletonMap("otel.metrics.exporter", "none")) .addPropertiesSupplier(() -> singletonMap("otel.traces.exporter", "none")) .addPropertiesSupplier(() -> singletonMap("otel.logs.exporter", "none")); diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/ScopeBasedPeriodicMetricReaderTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/ScopeBasedPeriodicMetricReaderTest.java new file mode 100644 index 0000000000..7bc64f25e8 --- /dev/null +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/ScopeBasedPeriodicMetricReaderTest.java @@ -0,0 +1,115 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.opentelemetry.javaagent.providers; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.CollectionRegistration; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.internal.data.ImmutableGaugeData; +import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ScopeBasedPeriodicMetricReaderTest { + @Mock private MetricExporter metricExporter; + @Mock private CollectionRegistration collectionRegistration; + + private ScopeBasedPeriodicMetricReader reader; + + @BeforeEach + public void setup() { + Set scopeNames = new HashSet<>(); + scopeNames.add("io.test.retained"); + reader = + ScopeBasedPeriodicMetricReader.create(metricExporter, scopeNames) + .setInterval(60, TimeUnit.SECONDS) + .build(); + reader.register(collectionRegistration); + } + + @Test + public void testScopeBasedMetricFilter() { + List metricDataList = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + metricDataList.add(getMetricData("io.test.retained")); + } + for (int i = 0; i < 3; i++) { + metricDataList.add(getMetricData("io.test.dropped")); + } + + Mockito.when(metricExporter.export(Mockito.anyList())) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.when(collectionRegistration.collectAllMetrics()).thenReturn(metricDataList); + + CompletableResultCode result = reader.forceFlush(); + + Mockito.verify(metricExporter).export(Mockito.argThat(list -> list.size() == 5)); + assertTrue(result.isSuccess()); + } + + @Test + public void testEmptyMetrics() { + Mockito.when(collectionRegistration.collectAllMetrics()).thenReturn(new ArrayList<>()); + + CompletableResultCode result = reader.forceFlush(); + + Mockito.verify(metricExporter, Mockito.never()).export(Mockito.anyList()); + assertTrue(result.isSuccess()); + } + + @Test + public void testShutdown() { + List metricDataList = new ArrayList<>(); + for (int i = 0; i < 1; i++) { + metricDataList.add(getMetricData("io.test.retained")); + } + + Mockito.when(metricExporter.export(Mockito.anyList())) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.when(metricExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + Mockito.when(collectionRegistration.collectAllMetrics()).thenReturn(metricDataList); + + CompletableResultCode result = reader.shutdown(); + + Mockito.verify(metricExporter).export(Mockito.argThat(list -> list.size() == 1)); + assertTrue(result.isSuccess()); + } + + private static MetricData getMetricData(String instrumentationScope) { + return ImmutableMetricData.createDoubleGauge( + Resource.empty(), + InstrumentationScopeInfo.create(instrumentationScope), + "test", + "", + "1", + ImmutableGaugeData.empty()); + } +} diff --git a/instrumentation/jmx-metrics/build.gradle.kts b/instrumentation/jmx-metrics/build.gradle.kts new file mode 100644 index 0000000000..6456230040 --- /dev/null +++ b/instrumentation/jmx-metrics/build.gradle.kts @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +plugins { + java + id("com.github.johnrengelman.shadow") +} + +base.archivesBaseName = "aws-instrumentation-jmx-metrics" diff --git a/instrumentation/jmx-metrics/src/main/resources/README.md b/instrumentation/jmx-metrics/src/main/resources/README.md new file mode 100644 index 0000000000..4c4d72cc38 --- /dev/null +++ b/instrumentation/jmx-metrics/src/main/resources/README.md @@ -0,0 +1,13 @@ +## JMX Metrics +The package provides all the configurations needed to have the [JMX Metric Insight](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/jmx-metrics/javaagent/README.md) +instrumentation support the same metrics as the [JMX Metric Gatherer](https://github.com/open-telemetry/opentelemetry-java-contrib/tree/main/jmx-metrics). + +It is required at least until [open-telemetry/opentelemetry-java-instrumentation#9765](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/9765) is addressed. + +``` +OTEL_EXPERIMENTAL_METRICS_VIEW_CONFIG: classpath:/jmx/view.yaml +``` + +### rules/*.yaml +The rules are a translation of the JMX Metric Gatherer's [target systems](https://github.com/open-telemetry/opentelemetry-java-contrib/tree/main/jmx-metrics/src/main/resources/target-systems) +based on the [JMX metric rule YAML schema](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/jmx-metrics/javaagent/README.md#basic-syntax). \ No newline at end of file diff --git a/instrumentation/jmx-metrics/src/main/resources/jmx/rules/jvm.yaml b/instrumentation/jmx-metrics/src/main/resources/jmx/rules/jvm.yaml new file mode 100644 index 0000000000..b2c29cfbc2 --- /dev/null +++ b/instrumentation/jmx-metrics/src/main/resources/jmx/rules/jvm.yaml @@ -0,0 +1,68 @@ +--- +rules: + - bean: java.lang:type=ClassLoading + unit: "1" + prefix: jvm.classes. + type: gauge + mapping: + LoadedClassCount: + metric: loaded + desc: Number of loaded classes + - bean: java.lang:type=GarbageCollector,* + prefix: jvm.gc.collections. + type: counter + metricAttribute: + name: param(name) + mapping: + CollectionCount: + metric: count + unit: "1" + desc: Total number of collections that have occurred + CollectionTime: + metric: elapsed + unit: ms + desc: The approximate accumulated collection elapsed time in milliseconds + - bean: java.lang:type=Memory + unit: by + prefix: jvm.memory. + type: gauge + mapping: + HeapMemoryUsage.used: + metric: heap.used + desc: The current heap usage + NonHeapMemoryUsage.used: + metric: nonheap.used + desc: The current non-heap usage + - bean: java.lang:type=MemoryPool,* + unit: by + prefix: jvm.memory.pool. + type: gauge + metricAttribute: + name: param(name) + mapping: + CollectionUsage.used: + metric: used_after_last_gc + desc: Memory used after the most recent gc event + Usage.used: + metric: used + desc: Current memory pool used + - bean: java.lang:type=Threading + unit: "1" + prefix: jvm.threads. + type: gauge + mapping: + ThreadCount: + metric: count + desc: Number of threads + - bean: java.lang:type=OperatingSystem,* + prefix: jvm.cpu. + type: gauge + mapping: + ProcessCpuTime: + metric: time + unit: ns + desc: CPU time used + ProcessCpuLoad: + metric: recent_utilization + unit: "1" + desc: Recent CPU utilization for the process diff --git a/otelagent/build.gradle.kts b/otelagent/build.gradle.kts index 6c224a32ce..2027a56bbf 100644 --- a/otelagent/build.gradle.kts +++ b/otelagent/build.gradle.kts @@ -62,6 +62,7 @@ dependencies { javaagentLibs(project(":awsagentprovider")) javaagentLibs(project(":instrumentation:log4j-2.13.2")) javaagentLibs(project(":instrumentation:logback-1.0")) + javaagentLibs(project(":instrumentation:jmx-metrics")) } tasks { diff --git a/settings.gradle.kts b/settings.gradle.kts index cc52e2b49c..f4ec99f115 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,6 +43,7 @@ include(":awspropagator") include(":dependencyManagement") include(":instrumentation:logback-1.0") include(":instrumentation:log4j-2.13.2") +include(":instrumentation:jmx-metrics") include(":otelagent") include(":smoke-tests:fakebackend") include(":smoke-tests:runner")