Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> mergeEnvWithSysProperties(Map<String, String> env, Properties props) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Resource, ConfigProperties, Resource> {
private static Resource resource;

@Getter private static Resource resource;

@Override
public Resource apply(Resource resource, ConfigProperties configProperties) {
Expand All @@ -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);
}
}
}
Comment thread
cleverchuk marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,6 +43,11 @@ class ResourceCustomizerTest {

@InjectMocks private ResourceCustomizer tested;

@AfterEach
void tearDown() {
ConfigManager.reset();
}

private final Resource resource =
Resource.create(
Attributes.builder()
Expand Down Expand Up @@ -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));
}
}
45 changes: 45 additions & 0 deletions pr-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Prefer detected service name to service key name in Azure App Service

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need this file checked in? ;)


## 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`.
1 change: 0 additions & 1 deletion smoke-tests/src/test/java/com/solarwinds/SmokeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading
Loading