diff --git a/build.gradle.kts b/build.gradle.kts index b0026e93..8b433ef7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,7 @@ plugins{ id("io.github.gradle-nexus.publish-plugin") version "2.0.0" } -val swoAgentVersion = "3.2.1" +val swoAgentVersion = "3.3.0" extra["swoAgentVersion"] = swoAgentVersion group = "com.solarwinds" version = if (System.getenv("SNAPSHOT_BUILD").toBoolean()) "$swoAgentVersion-SNAPSHOT" else swoAgentVersion diff --git a/custom/src/main/java/com/solarwinds/opentelemetry/extensions/config/ConfigurationLoader.java b/custom/src/main/java/com/solarwinds/opentelemetry/extensions/config/ConfigurationLoader.java index 0c6df7a4..b54eccab 100644 --- a/custom/src/main/java/com/solarwinds/opentelemetry/extensions/config/ConfigurationLoader.java +++ b/custom/src/main/java/com/solarwinds/opentelemetry/extensions/config/ConfigurationLoader.java @@ -165,32 +165,6 @@ private static void maybeFollowOtelConfigProperties(ConfigContainer configs) { logger.info("failed to follow Otel's log file config." + e.getMessage()); } } - - String serviceName = System.getProperty("otel.service.name"); - if (serviceName == null) { - serviceName = System.getenv("OTEL_SERVICE_NAME"); - } - - String serviceKey = (String) configs.get(ConfigProperty.AGENT_SERVICE_KEY); - if (serviceName == null) { - if (serviceKey != null) { - String name = ServiceKeyUtils.getServiceName(serviceKey); - if (name != null) { - System.setProperty("otel.service.name", name); - } - } - - } else { - if (serviceKey != null) { - try { - String key = String.format("%s:%s", ServiceKeyUtils.getApiKey(serviceKey), serviceName); - configs.put(ConfigProperty.AGENT_SERVICE_KEY, key, true); - } catch (InvalidConfigException e) { - LoggerFactory.getLogger() - .warn(String.format("Unable to update service name to %s", serviceName)); - } - } - } } static void configureOtel(ConfigContainer container) throws InvalidConfigException { diff --git a/custom/src/test/java/com/solarwinds/opentelemetry/extensions/config/ConfigurationLoaderTest.java b/custom/src/test/java/com/solarwinds/opentelemetry/extensions/config/ConfigurationLoaderTest.java index 28dd2379..2f574520 100644 --- a/custom/src/test/java/com/solarwinds/opentelemetry/extensions/config/ConfigurationLoaderTest.java +++ b/custom/src/test/java/com/solarwinds/opentelemetry/extensions/config/ConfigurationLoaderTest.java @@ -23,10 +23,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.solarwinds.joboe.config.ConfigContainer; -import com.solarwinds.joboe.config.ConfigManager; import com.solarwinds.joboe.config.ConfigProperty; import com.solarwinds.joboe.config.InvalidConfigException; -import com.solarwinds.joboe.config.ServiceKeyUtils; import com.solarwinds.opentelemetry.extensions.DefaultNamingScheme; import com.solarwinds.opentelemetry.extensions.NamingScheme; import com.solarwinds.opentelemetry.extensions.SpanAttributeNamingScheme; @@ -133,27 +131,6 @@ void testWindowsRootPath() { assertNull(ConfigurationLoader.getRuntimeConfigFilename()); } - @Test - @ClearSystemProperty(key = "otel.service.name") - void verifyThatOtelServiceNameSystemPropertyIsWhenNotExplicitlySet() - throws InvalidConfigException { - ConfigurationLoader.load(); - assertEquals( - "name" /*name is from custom/build.gradle test task*/, - System.getProperty("otel.service.name")); - } - - @Test - @SetSystemProperty(key = "otel.service.name", value = "test") - void verifyThatServiceKeyIsUpdatedWithOtelServiceNameWhenSystemPropertyIsSet() - throws InvalidConfigException { - ConfigurationLoader.load(); - String serviceKeyAfter = (String) ConfigManager.getConfig(ConfigProperty.AGENT_SERVICE_KEY); - - assertEquals("test", ServiceKeyUtils.getServiceName(serviceKeyAfter)); - assertEquals("token:test", serviceKeyAfter); - } - @Test @ClearSystemProperty(key = "otel.exporter.otlp.endpoint") @ClearSystemProperty(key = "otel.exporter.otlp.headers") diff --git a/libs/lambda/src/main/java/com/solarwinds/opentelemetry/extensions/LambdaConfigurationLoader.java b/libs/lambda/src/main/java/com/solarwinds/opentelemetry/extensions/LambdaConfigurationLoader.java index fb7f8d25..d2eafce9 100644 --- a/libs/lambda/src/main/java/com/solarwinds/opentelemetry/extensions/LambdaConfigurationLoader.java +++ b/libs/lambda/src/main/java/com/solarwinds/opentelemetry/extensions/LambdaConfigurationLoader.java @@ -110,32 +110,6 @@ private static void maybeFollowOtelConfigProperties(ConfigContainer configs) { logger.info("failed to follow Otel's log file config." + e.getMessage()); } } - - String serviceName = System.getProperty("otel.service.name"); - if (serviceName == null) { - serviceName = System.getenv("OTEL_SERVICE_NAME"); - } - - String serviceKey = (String) configs.get(ConfigProperty.AGENT_SERVICE_KEY); - if (serviceName == null) { - if (serviceKey != null) { - String name = ServiceKeyUtils.getServiceName(serviceKey); - if (name != null) { - System.setProperty("otel.service.name", name); - } - } - - } else { - if (serviceKey != null) { - try { - String key = String.format("%s:%s", ServiceKeyUtils.getApiKey(serviceKey), serviceName); - configs.put(ConfigProperty.AGENT_SERVICE_KEY, key, true); - } catch (InvalidConfigException e) { - LoggerFactory.getLogger() - .warn(String.format("Unable to update service name to %s", serviceName)); - } - } - } } static Map mergeEnvWithSysProperties(Map env, Properties props) { diff --git a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/ResourceCustomizer.java b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/ResourceCustomizer.java index a29efed1..44804802 100644 --- a/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/ResourceCustomizer.java +++ b/libs/shared/src/main/java/com/solarwinds/opentelemetry/extensions/ResourceCustomizer.java @@ -16,18 +16,25 @@ package com.solarwinds.opentelemetry.extensions; +import com.solarwinds.joboe.config.ConfigManager; +import com.solarwinds.joboe.config.ConfigProperty; +import com.solarwinds.joboe.config.InvalidConfigException; +import com.solarwinds.joboe.config.ServiceKeyUtils; import com.solarwinds.joboe.logging.LoggerFactory; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.ResourceBuilder; import io.opentelemetry.semconv.ServiceAttributes; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; import io.opentelemetry.semconv.incubating.ProcessIncubatingAttributes; import java.util.List; import java.util.function.BiFunction; import java.util.stream.Collectors; +import lombok.Getter; public class ResourceCustomizer implements BiFunction { - private static Resource resource; + + @Getter private static Resource resource; @Override public Resource apply(Resource resource, ConfigProperties configProperties) { @@ -51,16 +58,47 @@ public Resource apply(Resource resource, ConfigProperties configProperties) { resourceBuilder.put(ProcessIncubatingAttributes.PROCESS_COMMAND_ARGS, args); } + String serviceName = getServiceName(resource, configProperties); + updateServiceKey(serviceName); + resourceBuilder.put(ServiceAttributes.SERVICE_NAME, serviceName); + + LoggerFactory.getLogger().info("Resolved service name: " + serviceName); ResourceCustomizer.resource = resourceBuilder.build(); - LoggerFactory.getLogger() - .debug( - String.format( - "This log line is used for validation only: service.name: %s", - resource.getAttribute(ServiceAttributes.SERVICE_NAME))); return ResourceCustomizer.resource; } - public static Resource getResource() { - return resource; + private String getServiceName(Resource resource, ConfigProperties configProperties) { + String serviceKeyName = null; + String serviceKey = ConfigManager.getConfigOptional(ConfigProperty.AGENT_SERVICE_KEY, null); + if (serviceKey != null) { + serviceKeyName = ServiceKeyUtils.getServiceName(serviceKey); + } + + // Only allow detected name for azure app service + if (!"azure.app_service".equals(resource.getAttribute(CloudIncubatingAttributes.CLOUD_PLATFORM)) + && configProperties.getString("otel.service.name") == null) { + return serviceKeyName; + } + + String serviceName = resource.getAttribute(ServiceAttributes.SERVICE_NAME); + if (serviceKeyName != null && (serviceName == null || serviceName.startsWith("unknown_"))) { + serviceName = serviceKeyName; + } + + return serviceName; + } + + private void updateServiceKey(String serviceName) { + if (serviceName != null) { + String serviceKey = ConfigManager.getConfigOptional(ConfigProperty.AGENT_SERVICE_KEY, ":"); + String apiKey = ServiceKeyUtils.getApiKey(serviceKey); + + try { + ConfigManager.setConfig( + ConfigProperty.AGENT_SERVICE_KEY, String.format("%s:%s", apiKey, serviceName)); + } catch (InvalidConfigException ignore) { + LoggerFactory.getLogger().debug("Failed to update service key with name: " + serviceName); + } + } } } diff --git a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/ResourceCustomizerTest.java b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/ResourceCustomizerTest.java index 1851a382..b592d14c 100644 --- a/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/ResourceCustomizerTest.java +++ b/libs/shared/src/test/java/com/solarwinds/opentelemetry/extensions/ResourceCustomizerTest.java @@ -18,14 +18,21 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import com.solarwinds.joboe.config.ConfigManager; +import com.solarwinds.joboe.config.ConfigProperty; +import com.solarwinds.joboe.config.InvalidConfigException; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.ServiceAttributes; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; import io.opentelemetry.semconv.incubating.ProcessIncubatingAttributes; import java.util.Arrays; import java.util.Collections; import java.util.Objects; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -36,6 +43,11 @@ class ResourceCustomizerTest { @InjectMocks private ResourceCustomizer tested; + @AfterEach + void tearDown() { + ConfigManager.reset(); + } + private final Resource resource = Resource.create( Attributes.builder() @@ -104,4 +116,112 @@ void verifyThatProcessCommandLineIsNotModifiedWhenServiceKeyIsNotPresentProcessC Arrays.asList("-Duser.country=US", "-Duser.language=en"), actual.getAttribute(ProcessIncubatingAttributes.PROCESS_COMMAND_ARGS)); } + + @Test + void verifyThatServiceKeyNameIsPreferredOverDetectedNameByDefault() + throws InvalidConfigException { + ConfigManager.setConfig(ConfigProperty.AGENT_SERVICE_KEY, "token:key-service"); + Resource resource = + Resource.create( + Attributes.builder().put(ServiceAttributes.SERVICE_NAME, "my-service").build()); + + Resource actual = + tested.apply(resource, DefaultConfigProperties.createFromMap(Collections.emptyMap())); + assertEquals("key-service", actual.getAttribute(ServiceAttributes.SERVICE_NAME)); + } + + @Test + void verifyThatDetectedServiceNameIsUsedForAzureAppService() throws InvalidConfigException { + ConfigManager.setConfig(ConfigProperty.AGENT_SERVICE_KEY, "token:key-service"); + Resource resource = + Resource.create( + Attributes.builder() + .put(CloudIncubatingAttributes.CLOUD_PLATFORM, "azure.app_service") + .put(ServiceAttributes.SERVICE_NAME, "my-service") + .build()); + + Resource actual = + tested.apply(resource, DefaultConfigProperties.createFromMap(Collections.emptyMap())); + assertEquals("my-service", actual.getAttribute(ServiceAttributes.SERVICE_NAME)); + } + + @Test + void verifyThatServiceKeyNameIsUsedForAzureAppServiceWhenDetectedNameIsUnknown() + throws InvalidConfigException { + ConfigManager.setConfig(ConfigProperty.AGENT_SERVICE_KEY, "token:key-service"); + Resource resource = + Resource.create( + Attributes.builder() + .put(CloudIncubatingAttributes.CLOUD_PLATFORM, "azure.app_service") + .put(ServiceAttributes.SERVICE_NAME, "unknown_service:java") + .build()); + + Resource actual = + tested.apply(resource, DefaultConfigProperties.createFromMap(Collections.emptyMap())); + assertEquals("key-service", actual.getAttribute(ServiceAttributes.SERVICE_NAME)); + } + + @Test + void verifyThatServiceKeyNameIsUsedForAzureAppServiceWhenDetectedNameIsNull() + throws InvalidConfigException { + ConfigManager.setConfig(ConfigProperty.AGENT_SERVICE_KEY, "token:key-service"); + Resource resource = + Resource.create( + Attributes.builder() + .put(CloudIncubatingAttributes.CLOUD_PLATFORM, "azure.app_service") + .build()); + + Resource actual = + tested.apply(resource, DefaultConfigProperties.createFromMap(Collections.emptyMap())); + assertEquals("key-service", actual.getAttribute(ServiceAttributes.SERVICE_NAME)); + } + + @Test + void verifyThatServiceKeyNameIsUsedByDefaultWhenResourceHasNoServiceName() + throws InvalidConfigException { + ConfigManager.setConfig(ConfigProperty.AGENT_SERVICE_KEY, "token:key-service"); + Resource resource = Resource.create(Attributes.empty()); + + Resource actual = + tested.apply(resource, DefaultConfigProperties.createFromMap(Collections.emptyMap())); + assertEquals("key-service", actual.getAttribute(ServiceAttributes.SERVICE_NAME)); + } + + @Test + void verifyThatServiceNameIsNullWhenNotInResourceAndNoServiceKeyConfigured() { + Resource resource = Resource.create(Attributes.empty()); + Resource actual = + tested.apply(resource, DefaultConfigProperties.createFromMap(Collections.emptyMap())); + + assertNull(actual.getAttribute(ServiceAttributes.SERVICE_NAME)); + } + + @Test + void verifyThatServiceKeyIsUpdatedWithServiceName() throws InvalidConfigException { + ConfigManager.setConfig(ConfigProperty.AGENT_SERVICE_KEY, "token:old-name"); + Resource resource = + Resource.create( + Attributes.builder().put(ServiceAttributes.SERVICE_NAME, "new-name").build()); + + tested.apply( + resource, + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.service.name", "new-name"))); + assertEquals("token:new-name", ConfigManager.getConfig(ConfigProperty.AGENT_SERVICE_KEY)); + } + + @Test + void verifyThatServiceKeyIsUpdatedWithServiceNameWhenOnAppService() + throws InvalidConfigException { + ConfigManager.setConfig(ConfigProperty.AGENT_SERVICE_KEY, "token:old-name"); + Resource resource = + Resource.create( + Attributes.builder() + .put(CloudIncubatingAttributes.CLOUD_PLATFORM, "azure.app_service") + .put(ServiceAttributes.SERVICE_NAME, "new-name") + .build()); + + tested.apply(resource, DefaultConfigProperties.createFromMap(Collections.emptyMap())); + assertEquals("token:new-name", ConfigManager.getConfig(ConfigProperty.AGENT_SERVICE_KEY)); + } } diff --git a/pr-description.md b/pr-description.md new file mode 100644 index 00000000..3b0ad068 --- /dev/null +++ b/pr-description.md @@ -0,0 +1,45 @@ +# Prefer detected service name to service key name in Azure App Service + +## Summary + +Centralizes service name resolution into `ResourceCustomizer` and introduces environment-aware precedence: on **Azure App Service**, the OTel-detected service name takes priority over the name embedded in the service key. For all other environments, the service key name continues to win. The service key is then updated to stay in sync with the resolved name. + +--- + +## What changed and why + +### Service name resolution moved to `ResourceCustomizer` + +The service name synchronization logic was previously duplicated in both `ConfigurationLoader` and `LambdaConfigurationLoader`. It ran during config loading — before OTel resource detection — which meant it could not see Azure-injected resource attributes. Both copies have been removed, and the logic now lives entirely in `ResourceCustomizer.apply()`, which runs after resource detection. + +### Azure App Service gets detected name precedence + +Azure App Service uses OTel resource detection to inject a meaningful `service.name` (the app name). The old logic always overwrote this with the service key name. + +The new `getServiceName` method applies this rule: + +- **Default (non-Azure, no explicit `otel.service.name`)**: service key name wins. +- **Azure App Service** (`cloud.platform = azure.app_service`): detected name wins, _unless_ it is `null` or starts with `unknown_` (i.e., the OTel SDK fallback placeholder), in which case the service key name is used. +- **Explicit `otel.service.name` set**: detected/config name wins in all environments. + +### Service key kept in sync + +After resolving the final service name, `updateServiceKey()` rewrites the service key as `{apiKey}:{resolvedName}`. This ensures the collector receives a consistent key regardless of which name source won. + +### Dependency exclusions for `opentelemetry-cel-sampler` + +Guava and protobuf are now excluded from the `opentelemetry-cel-sampler` transitive dependency tree. Both are already on the classpath via the agent and were causing version conflicts at runtime. + +### Smoke test validation log removed + +The old debug log line (`"This log line is used for validation only: service.name: ..."`) has been replaced with a proper `info` log (`"Resolved service name: ..."`) and the smoke test assertions referencing the old line have been removed. + +### `HttpSettingsReader` debug log enriched + +The debug log now includes `serviceName` and `hostname` alongside the fetched settings, making it easier to correlate settings fetches with the service identity at the time of the call. + +--- + +## Version + +Bumped `swoAgentVersion` from `3.2.0` → `3.3.0`. diff --git a/smoke-tests/src/test/java/com/solarwinds/SmokeTest.java b/smoke-tests/src/test/java/com/solarwinds/SmokeTest.java index 044baa0f..f5a0ebc7 100644 --- a/smoke-tests/src/test/java/com/solarwinds/SmokeTest.java +++ b/smoke-tests/src/test/java/com/solarwinds/SmokeTest.java @@ -60,7 +60,6 @@ public class SmokeTest { "hostId:.*[0-9a-z-]+", "Extension attached!", "trace_id=[a-z0-9]+\\s+span_id=[a-z0-9]+\\s+trace_flags=[0-9a-f]{2}", - "This log line is used for validation only: service.name: java-apm-smoke-test", "Applying instrumentation: sw-jdbc", "Clearing transaction name buffer. Unique transaction count: \\d+", "CONNECT otel.collector.na-01.st-ssp.solarwinds.com:443") diff --git a/smoke-tests/src/test/java/com/solarwinds/SmokeTestV2.java b/smoke-tests/src/test/java/com/solarwinds/SmokeTestV2.java index 9d8b6096..3ef3fb16 100644 --- a/smoke-tests/src/test/java/com/solarwinds/SmokeTestV2.java +++ b/smoke-tests/src/test/java/com/solarwinds/SmokeTestV2.java @@ -60,7 +60,6 @@ public class SmokeTestV2 { "hostId:.*[0-9a-z-]+", "Extension attached!", "trace_id=[a-z0-9]+\\s+span_id=[a-z0-9]+\\s+trace_flags=[0-9a-f]{2}", - "This log line is used for validation only: service.name: java-apm-smoke-test", "Applying instrumentation: sw-jdbc", "Clearing transaction name buffer. Unique transaction count: \\d+") , new Slf4jLogConsumer(LoggerFactory.getLogger("k6")));