001package io.prometheus.otelagent;
002
003import static java.nio.file.Files.createTempDirectory;
004
005import java.io.File;
006import java.io.InputStream;
007import java.lang.reflect.Field;
008import java.lang.reflect.Method;
009import java.net.URL;
010import java.net.URLClassLoader;
011import java.nio.file.Files;
012import java.nio.file.Path;
013import java.nio.file.StandardCopyOption;
014import java.util.Collections;
015import java.util.HashMap;
016import java.util.Map;
017
018public class ResourceAttributesFromOtelAgent {
019
020  private static final String[] OTEL_JARS =
021      new String[] {"opentelemetry-api-1.29.0.jar", "opentelemetry-context-1.29.0.jar"};
022
023  /**
024   * This grabs resource attributes like {@code service.name} and {@code service.instance.id} from
025   * the OTel Java agent (if present) and adds them to {@code result}.
026   *
027   * <p>The way this works is as follows: If the OTel Java agent is attached, it modifies the {@code
028   * GlobalOpenTelemetry.get()} method to return an agent-specific object. From that agent-specific
029   * object we can get the resource attributes via reflection.
030   *
031   * <p>So we load the {@code GlobalOpenTelemetry} class (in a separate class loader from the JAR
032   * files that are bundled with this module), call {@code .get()}, and inspect the returned object.
033   *
034   * <p>After that we discard the class loader so that all OTel specific classes are unloaded. No
035   * runtime dependency on any OTel version remains.
036   *
037   * <p>The test for this class is in
038   * examples/example-exporter-opentelemetry/oats-tests/agent/service-instance-id-check.py
039   */
040  public static Map<String, String> getResourceAttributes(String instrumentationScopeName) {
041    try {
042      Path tmpDir = createTempDirectory(instrumentationScopeName + "-");
043      try {
044        URL[] otelJars = copyOtelJarsToTempDir(tmpDir, instrumentationScopeName);
045
046        try (URLClassLoader classLoader = new URLClassLoader(otelJars)) {
047          Class<?> globalOpenTelemetryClass =
048              classLoader.loadClass("io.opentelemetry.api.GlobalOpenTelemetry");
049          Object globalOpenTelemetry = globalOpenTelemetryClass.getMethod("get").invoke(null);
050          if (globalOpenTelemetry.getClass().getSimpleName().contains("ApplicationOpenTelemetry")) {
051            // GlobalOpenTelemetry is injected by the OTel Java agent
052            Object applicationMeterProvider = callMethod("getMeterProvider", globalOpenTelemetry);
053            Object agentMeterProvider = getField("agentMeterProvider", applicationMeterProvider);
054            Object sdkMeterProvider = getField("delegate", agentMeterProvider);
055            Object sharedState = getField("sharedState", sdkMeterProvider);
056            Object resource = callMethod("getResource", sharedState);
057            Object attributes = callMethod("getAttributes", resource);
058            Map<?, ?> attributeMap = (Map<?, ?>) callMethod("asMap", attributes);
059
060            Map<String, String> result = new HashMap<>();
061            for (Map.Entry<?, ?> entry : attributeMap.entrySet()) {
062              if (entry.getKey() != null && entry.getValue() != null) {
063                result.put(entry.getKey().toString(), entry.getValue().toString());
064              }
065            }
066            return Collections.unmodifiableMap(result);
067          }
068        }
069      } finally {
070        deleteTempDir(tmpDir.toFile());
071      }
072    } catch (Exception ignored) {
073      // ignore
074    }
075    return Collections.emptyMap();
076  }
077
078  private static Object getField(String name, Object obj) throws Exception {
079    Field field = obj.getClass().getDeclaredField(name);
080    field.setAccessible(true);
081    return field.get(obj);
082  }
083
084  private static Object callMethod(String name, Object obj) throws Exception {
085    Method method = obj.getClass().getMethod(name);
086    method.setAccessible(true);
087    return method.invoke(obj);
088  }
089
090  private static URL[] copyOtelJarsToTempDir(Path tmpDir, String instrumentationScopeName)
091      throws Exception {
092    URL[] result = new URL[OTEL_JARS.length];
093    for (int i = 0; i < OTEL_JARS.length; i++) {
094      InputStream inputStream =
095          Thread.currentThread().getContextClassLoader().getResourceAsStream("lib/" + OTEL_JARS[i]);
096      if (inputStream == null) {
097        throw new IllegalStateException(
098            "Error initializing "
099                + instrumentationScopeName
100                + ": lib/"
101                + OTEL_JARS[i]
102                + " not found in classpath.");
103      }
104      File outputFile = tmpDir.resolve(OTEL_JARS[i]).toFile();
105      Files.copy(inputStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
106      inputStream.close();
107      result[i] = outputFile.toURI().toURL();
108    }
109    return result;
110  }
111
112  private static void deleteTempDir(File tmpDir) {
113    // We don't have subdirectories, so this simple implementation should work.
114    for (File file : tmpDir.listFiles()) {
115      file.delete();
116    }
117    tmpDir.delete();
118  }
119}