Skip to content
Closed
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
1 change: 1 addition & 0 deletions agent/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {

bootstrapLibs(project(":bootstrap"))
bootstrapLibs(project(":libs:core"))
bootstrapLibs(project(path = ":libs:resource", configuration = "shadow"))

bootstrapLibs(project(":libs:config"))
bootstrapLibs(project(":libs:sampling"))
Expand Down
1 change: 1 addition & 0 deletions custom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
testImplementation(project(":libs:sampling"))
testImplementation(project(":libs:shared"))
testImplementation(project(":libs:core"))
testRuntimeOnly(project(path = ":libs:resource", configuration = "shadow"))

testImplementation("io.opentelemetry:opentelemetry-api-incubator")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator")
Expand Down
3 changes: 3 additions & 0 deletions dependencyManagement/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ dependencies {
api("org.junit.jupiter:junit-jupiter-engine:$junit5")

api("io.opentelemetry:opentelemetry-api:$otelSdkVersion")
api("io.opentelemetry:opentelemetry-api-incubator:$opentelemetryAlpha")
api("io.opentelemetry.javaagent:opentelemetry-javaagent:$otelAgentVersion")
api("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:$otelSdkVersion")
api("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:$otelSdkVersion")

api("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:$otelAgentVersion")
api("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling:$opentelemetryJavaagentAlpha")
Expand All @@ -65,6 +67,7 @@ dependencies {

api("io.opentelemetry.contrib:opentelemetry-span-stacktrace:$otelJavaContribVersion")
api("io.opentelemetry.contrib:opentelemetry-cel-sampler:$otelJavaContribVersion")
api("io.opentelemetry.contrib:opentelemetry-azure-resources:$otelJavaContribVersion")
api("io.opentelemetry.semconv:opentelemetry-semconv-incubating:$opentelemetrySemconvAlpha")

api("io.opentelemetry:opentelemetry-api-incubator:$opentelemetryAlpha")
Expand Down
6 changes: 6 additions & 0 deletions libs/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ dependencies {
compileOnly("io.opentelemetry:opentelemetry-api")
compileOnly("io.opentelemetry:opentelemetry-context")

// Azure resource detection is provided by the shaded :libs:resource module. Only the facade
// (plain JDK types) is needed at compile time; the relocated, self-contained shaded jar is
// placed on the bootstrap class loader by the agent build at runtime.
compileOnly(project(":libs:resource"))

implementation("javax.xml.bind:jaxb-api:2.3.1")

implementation("com.solarwinds:apm-proto:1.0.8") {
Expand All @@ -32,6 +37,7 @@ dependencies {
testImplementation("org.json:json")
testImplementation("io.opentelemetry:opentelemetry-api")
testImplementation("io.opentelemetry:opentelemetry-context")
testRuntimeOnly(project(path = ":libs:resource", configuration = "shadow"))
}

sourceSets {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.solarwinds.joboe.core.HostId;
import com.solarwinds.joboe.logging.Logger;
import com.solarwinds.joboe.logging.LoggerFactory;
import com.solarwinds.joboe.resource.AzureResourceReader;
import io.opentelemetry.api.internal.InstrumentationUtil;
import java.io.BufferedReader;
import java.io.File;
Expand Down Expand Up @@ -1098,7 +1099,6 @@ public static class AzureReader {
@Getter(lazy = true)
private static final AzureReader instance = new AzureReader();

private static final String DEFAULT_METADATA_VERSION = "2021-12-13";
private final String appInstanceId;

private final HostId.AzureVmMetadata azureVmMetadata;
Expand All @@ -1107,71 +1107,105 @@ public static String getAppInstanceId() {
return getInstance().appInstanceId;
}

private HostId.AzureVmMetadata getVmMetadata() {
Integer timeout =
ConfigManager.getConfigOptional(
ConfigProperty.AGENT_AZURE_VM_METADATA_TIMEOUT, TIMEOUT_DEFAULT);
String metadataVersionCfg =
(String) ConfigManager.getConfig(ConfigProperty.AGENT_AZURE_VM_METADATA_VERSION);

final String metadataVersion =
metadataVersionCfg != null ? metadataVersionCfg : DEFAULT_METADATA_VERSION;
AtomicReference<HostId.AzureVmMetadata> result = new AtomicReference<>();

InstrumentationUtil.suppressInstrumentation(
() -> {
HttpURLConnection connection = null;
try {
URL url =
new URL(
String.format(
"%s%s%s",
METADATA_SERVICE_URL,
"/metadata/instance?api-version=",
metadataVersion));

connection = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
connection.setRequestMethod("GET");
connection.setReadTimeout(timeout);

connection.setConnectTimeout(timeout);
connection.setRequestProperty("Metadata", "true");
int statusCode = connection.getResponseCode();
private static HostId.AzureVmMetadata buildVmMetadata(Map<String, String> attrs) {
String hostId = attrs.get("host.id");
if (hostId == null) {
return null;
}
String cloudAccountId = null;
String azureResourceGroupName = null;
String resourceId = attrs.get("cloud.resource_id");
if (resourceId != null) {
String[] parts = resourceId.split("/");
if (parts.length > 2) {
cloudAccountId = parts[2];
}
if (parts.length > 4) {
azureResourceGroupName = parts[4];
}
}
String hostName = attrs.get("host.name");
return HostId.AzureVmMetadata.builder()
.cloudPlatform("azure.vm")
.cloudRegion(attrs.get("cloud.region"))
.hostId(hostId)
.hostName(hostName)
.azureVmName(hostName)
.cloudAccountId(cloudAccountId)
.azureVmSize(attrs.get("azure.vm.sku"))
.azureVmScaleSetName(attrs.get("azure.vm.scaleset.name"))
.azureResourceGroupName(azureResourceGroupName)
.build();
}

logger.debug(String.format("Azure IMDS status code: %s", statusCode));
if (statusCode >= 200 && statusCode < 300) {
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
StringBuilder sb = new StringBuilder();
private static HostId.AzureVmMetadata buildAppServiceMetadata(Map<String, String> attrs) {
String cloudAccountId = null;
String azureResourceGroupName = null;
String resourceId = attrs.get("cloud.resource_id");
if (resourceId != null) {
String[] parts = resourceId.split("/");
if (parts.length > 2) {
cloudAccountId = parts[2];
}
if (parts.length > 4) {
azureResourceGroupName = parts[4];
}
}
return HostId.AzureVmMetadata.builder()
.cloudPlatform("azure.app_service")
.cloudRegion(attrs.get("cloud.region"))
.hostId(attrs.get("service.instance.id"))
.hostName(attrs.get("host.id"))
.cloudAccountId(cloudAccountId)
.azureResourceGroupName(azureResourceGroupName)
.build();
}

String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
private static HostId.AzureVmMetadata buildFunctionsMetadata(Map<String, String> attrs) {
return HostId.AzureVmMetadata.builder()
.cloudPlatform("azure.functions")
.cloudRegion(attrs.get("cloud.region"))
.hostId(attrs.get("faas.instance"))
.hostName(attrs.get("faas.name"))
.build();
}

String payload = sb.toString();
logger.debug(String.format("Azure IMDS payload: %s", payload));
result.set(HostId.AzureVmMetadata.fromJson(payload));
}
}
private static HostId.AzureVmMetadata buildContainerAppMetadata(Map<String, String> attrs) {
return HostId.AzureVmMetadata.builder()
.cloudPlatform("azure.container_apps")
.hostId(attrs.get("service.instance.id"))
.hostName(attrs.get("service.name"))
.build();
}

} catch (IOException | JSONException exception) {
logger.debug("Error retrieving vmId from IMDS", exception);
} finally {
if (connection != null) {
connection.disconnect();
}
}
});
return result.get();
private HostId.AzureVmMetadata getAzureMetadata(String platform) {
AtomicReference<Map<String, String>> attrsRef = new AtomicReference<>();
InstrumentationUtil.suppressInstrumentation(
() -> attrsRef.set(AzureResourceReader.detectAttributes()));
Map<String, String> attrs = attrsRef.get();
if (attrs == null || attrs.isEmpty()) {
return null;
}
switch (platform) {
case "APP_SERVICE":
return buildAppServiceMetadata(attrs);
case "FUNCTIONS":
return buildFunctionsMetadata(attrs);
case "CONTAINER_APP":
return buildContainerAppMetadata(attrs);
case "NONE":
default:
return buildVmMetadata(attrs);
}
}

private AzureReader() {
this.appInstanceId = System.getenv(INSTANCE_ID_ENV_VARIABLE);
if (this.appInstanceId != null) {
logger.debug("Found Azure instance ID: " + this.appInstanceId);
}
azureVmMetadata = getVmMetadata();
String platform = AzureResourceReader.detectPlatform();
this.azureVmMetadata = getAzureMetadata(platform);
logger.debug(String.format("Azure vm metadata: %s", azureVmMetadata));
}
}
Expand Down
58 changes: 58 additions & 0 deletions libs/resource/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* © SolarWinds Worldwide, LLC. All rights reserved.
*
* 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.
*/

import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
id("solarwinds.java-conventions")
id("com.gradleup.shadow")
}

description = "shaded azure resource detection"

// The OpenTelemetry azure-resources detector is only consumable through the SDK
// autoconfigure SPI (ComponentProvider / ConditionalResourceProvider). Those SPI
// classes are not resolvable from the joboe core bootstrap class loader at premain.
// To use the detector from there, we bundle it together with the OpenTelemetry SDK
// it depends on and relocate everything into a private namespace so it is fully
// self-contained and cannot collide with the agent's own (unrelocated) SDK that
// lives in the agent class loader.
dependencies {
implementation("io.opentelemetry.contrib:opentelemetry-azure-resources")
implementation("io.opentelemetry:opentelemetry-sdk")
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
implementation("io.opentelemetry:opentelemetry-api")
implementation("io.opentelemetry:opentelemetry-api-incubator")
implementation("io.opentelemetry.semconv:opentelemetry-semconv")
implementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
}

tasks.withType<Test>().configureEach {
jvmArgs(
"--add-opens=java.base/java.lang=ALL-UNNAMED",
"--add-opens=java.base/java.util=ALL-UNNAMED"
)
}

tasks.withType<ShadowJar>().configureEach {
mergeServiceFiles()
// Relocate the bundled OpenTelemetry SDK + azure detector and its Jackson
// dependency into a private namespace. The public facade in
// com.solarwinds.joboe.resource is intentionally left unrelocated.
relocate("io.opentelemetry", "com.solarwinds.joboe.shaded.azure.io.opentelemetry")
relocate("com.fasterxml.jackson", "com.solarwinds.joboe.shaded.azure.com.fasterxml.jackson")
exclude("**/module-info.class")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* © SolarWinds Worldwide, LLC. All rights reserved.
*
* 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.solarwinds.joboe.resource;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.contrib.azure.resource.AzureEnvVarPlatform;
import io.opentelemetry.contrib.azure.resource.AzureResourceDetector;
import io.opentelemetry.sdk.resources.Resource;
import java.util.HashMap;
import java.util.Map;

/**
* Facade over the OpenTelemetry {@code azure-resources} detector.
*
* <p>This class is the single point that touches the OpenTelemetry SDK autoconfigure SPI. All
* OpenTelemetry types referenced here are relocated into a private namespace when this module is
* shaded, so callers receive only plain JDK types and never link against the SPI classes that are
* unavailable in the joboe core bootstrap class loader.
*/
public final class AzureResourceReader {

private AzureResourceReader() {}

/**
* Detects the Azure platform from environment variables.
*
* @return the platform name, one of {@code APP_SERVICE}, {@code FUNCTIONS}, {@code CONTAINER_APP}
* or {@code NONE}
*/
public static String detectPlatform() {
return AzureEnvVarPlatform.detect(System.getenv()).name();
}

/**
* Runs the Azure resource detector and flattens the detected resource into a string map keyed by
* OpenTelemetry attribute key.
*
* <p>This may perform an HTTP call to the Azure instance metadata service for the plain VM case.
* Callers are responsible for suppressing instrumentation around this call.
*
* @return a map of attribute key to value; empty if nothing is detected
*/
public static Map<String, String> detectAttributes() {
Resource resource = new AzureResourceDetector().create(DeclarativeConfigProperties.empty());
Attributes attributes = resource.getAttributes();
Map<String, String> result = new HashMap<>(attributes.size());
attributes.forEach((key, value) -> result.put(key.getKey(), String.valueOf(value)));
return result;
}
}
Loading
Loading