Skip to content

Commit

Permalink
Fix crashes and ugly error logs on unsupported systems for UP integra…
Browse files Browse the repository at this point in the history
…tion (#313)

* Fix crashes and ugly error logs on unsupported systems

* Typo fix

* spotless
  • Loading branch information
JonasKunz authored Jul 3, 2024
1 parent 581df01 commit ef59dc0
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 26 deletions.
126 changes: 107 additions & 19 deletions jvmti-access/src/main/java/co/elastic/otel/JvmtiAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@
*/
package co.elastic.otel;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
Expand All @@ -29,6 +34,8 @@ public class JvmtiAccess {

private static final Logger logger = Logger.getLogger(JvmtiAccess.class.getName());

private static volatile LibraryLookupResult libraryLookupResult = null;

private enum State {
NOT_LOADED,
LOAD_FAILED,
Expand Down Expand Up @@ -82,7 +89,7 @@ public static void ensureInitialized() {
doInit();
}
if (state != State.INITIALIZED) {
throw new IllegalStateException("Agent could not be initialized");
throw new IllegalStateException("jvmti native library could not be initialized");
}
}

Expand Down Expand Up @@ -125,42 +132,123 @@ public static synchronized void destroy() {

private static void checkError(int returnCode) {
if (returnCode < 0) {
throw new RuntimeException("Elastic JVMTI Agent returned error code " + returnCode);
throw new RuntimeException("Elastic jvmti native library returned error code " + returnCode);
}
}

/**
* @return null, if the current system is supported. Otherwise a String with a reason why the
* system is not supported.
*/
@Nullable
public static String getSystemUnsupportedReason() {
return lookupLibrary().failureReason;
}

private static void loadNativeLibrary() {
LibraryLookupResult lib = lookupLibrary();
if (lib.failureReason != null) {
throw new IllegalStateException(lib.failureReason);
}

String libraryDirectory = System.getProperty("java.io.tmpdir");
String libraryName = "elastic-jvmti-" + lib.libraryName;
Path file =
ResourceExtractionUtil.extractResourceToDirectory(
"elastic-jvmti/" + libraryName + ".so",
libraryName,
".so",
Paths.get(libraryDirectory));
System.load(file.toString());
}

private static LibraryLookupResult lookupLibrary() {
if (libraryLookupResult == null) {
libraryLookupResult = doLookupLibrary();
}
return libraryLookupResult;
}

private static LibraryLookupResult doLookupLibrary() {
String os = System.getProperty("os.name").toLowerCase();
String arch = System.getProperty("os.arch").toLowerCase();
String libraryName;
if (os.contains("linux")) {
if (isMusl()) {
return LibraryLookupResult.failure("Musl Linux is not supported yet");
}
if (arch.contains("arm") || arch.contains("aarch32")) {
throw new IllegalStateException("Unsupported architecture for Linux: " + arch);
return LibraryLookupResult.failure("Unsupported architecture for Linux: " + arch);
} else if (arch.contains("aarch")) {
libraryName = "linux-arm64";
return LibraryLookupResult.success("linux-arm64");
} else if (arch.contains("64")) {
libraryName = "linux-x64";
return LibraryLookupResult.success("linux-x64");
} else {
throw new IllegalStateException("Unsupported architecture for Linux: " + arch);
return LibraryLookupResult.failure("Unsupported architecture for Linux: " + arch);
}
} else if (os.contains("mac")) {
if (arch.contains("aarch")) {
libraryName = "darwin-arm64";
return LibraryLookupResult.success("darwin-arm64");
} else {
libraryName = "darwin-x64";
return LibraryLookupResult.success("darwin-x64");
}
} else {
throw new IllegalStateException("Native agent does not work on " + os);
return LibraryLookupResult.failure("jvmti native library does not work on " + os);
}
}

String libraryDirectory = System.getProperty("java.io.tmpdir");
libraryName = "elastic-jvmti-" + libraryName;
Path file =
ResourceExtractionUtil.extractResourceToDirectory(
"elastic-jvmti/" + libraryName + ".so",
libraryName,
".so",
Paths.get(libraryDirectory));
System.load(file.toString());
// The isMusl() and isAlpineLinux() methods are based on the approach taken by the sqlite-jdbc
// project:
// https://github.com/xerial/sqlite-jdbc/pull/675
private static boolean isMusl() {
Path mapFilesDir = Paths.get("/proc/self/map_files");
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(mapFilesDir)) {
for (Path file : dirStream) {
try {
if (file.toRealPath().toString().toLowerCase().contains("musl")) {
return true;
}
} catch (IOException e) {
// ignore
}
}
return false;
} catch (Exception ignored) {
// fall back to checking for alpine linux in the event we're using an older kernel which
// may not fail the above check
return isAlpineLinux();
}
}

private static boolean isAlpineLinux() {
try {
List<String> lines = Files.readAllLines(Paths.get("/etc/os-release"), StandardCharsets.UTF_8);
for (String l : lines) {
if (l.startsWith("ID") && l.contains("alpine")) {
return true;
}
}
} catch (Exception ignored) {
}
return false;
}

private static class LibraryLookupResult {

// Either failureReason or libraryName are not null
@Nullable final String failureReason;
@Nullable final String libraryName;

public LibraryLookupResult(@Nullable String libraryName, @Nullable String failureReason) {
this.failureReason = failureReason;
this.libraryName = libraryName;
}

static LibraryLookupResult failure(String reason) {
return new LibraryLookupResult(null, reason);
}

static LibraryLookupResult success(String libraryName) {
return new LibraryLookupResult(libraryName, null);
}
}
}
10 changes: 5 additions & 5 deletions universal-profiling-integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ This extension supports [autoconfiguration](https://github.com/open-telemetry/op

So if you are using an autoconfigured OpenTelemetry SDK, you'll only need to add this extension to your class path and configure it via system properties or environment variables:

| Property Name / Environment Variable Name | Default | Description |
|-------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| elastic.otel.universal.profiling.integration.enabled <br/> ELASTIC_OTEL_UNIVERSAL_PROFILING_INTEGRATION_ENABLED | `auto` | Enables or disables the feature. Possible values are `true`, `false` or `auto`. On `auto` the profiling integration will be installed but remain inactive until the presence of a profiler is detected (Requires a profiling host agent 8.15 or later). This reduces the overhead in the case no profiler is there. When using `auto`, there might be a slight delay until the correlation is activated. So if your application creates spans during startup which you want correlated, you should use `true` instead. |
| elastic.otel.universal.profiling.integration.socket.dir <br/> ELASTIC_OTEL_UNIVERSAL_PROFILING_INTEGRATION_SOCKET_DIR | the value of the `java.io.tmpdir` JVM-property | The extension needs to bind a socket to a file for communicating with the universal profiling host agent. By default, this socket will be placed in the java.io.tmpdir. This configuration option can be used to change the location. Note that the total path name (including the socket) must not exceed 100 characters due to OS restrictions. |
| elastic.otel.universal.profiling.integration.buffer.size <br/> ELASTIC_OTEL_UNIVERSAL_PROFILING_INTEGRATION_BUFFER_SIZE | 8096 | The extension needs to buffer ended local-root spans for a short duration to ensure that all of its profiling data has been received. This configuration options configures the buffer size in number of spans. The higher the number of local root spans per second, the higher this buffer size should be set. The extension will log a warning if it is not capable of buffering a span due to insufficient buffer size. This will cause the span to be exported immediately instead with possibly incomplete profiling correlation data. |
| Property Name / Environment Variable Name | Default | Description |
|-------------------------------------------------------------------------------------------------------------------------|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| elastic.otel.universal.profiling.integration.enabled <br/> ELASTIC_OTEL_UNIVERSAL_PROFILING_INTEGRATION_ENABLED | `auto` on supported systems, `false` otherwise | Enables or disables the feature. Possible values are `true`, `false` or `auto`. On `auto` the profiling integration will be installed but remain inactive until the presence of a profiler is detected (Requires a profiling host agent 8.15 or later). This reduces the overhead in the case no profiler is there. When using `auto`, there might be a slight delay until the correlation is activated. So if your application creates spans during startup which you want correlated, you should use `true` instead. |
| elastic.otel.universal.profiling.integration.socket.dir <br/> ELASTIC_OTEL_UNIVERSAL_PROFILING_INTEGRATION_SOCKET_DIR | the value of the `java.io.tmpdir` JVM-property | The extension needs to bind a socket to a file for communicating with the universal profiling host agent. By default, this socket will be placed in the java.io.tmpdir. This configuration option can be used to change the location. Note that the total path name (including the socket) must not exceed 100 characters due to OS restrictions. |
| elastic.otel.universal.profiling.integration.buffer.size <br/> ELASTIC_OTEL_UNIVERSAL_PROFILING_INTEGRATION_BUFFER_SIZE | 8096 | The extension needs to buffer ended local-root spans for a short duration to ensure that all of its profiling data has been received. This configuration options configures the buffer size in number of spans. The higher the number of local root spans per second, the higher this buffer size should be set. The extension will log a warning if it is not capable of buffering a span due to insufficient buffer size. This will cause the span to be exported immediately instead with possibly incomplete profiling correlation data. |


### Manual SDK setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,17 @@ private enum EnabledOptions {
public void registerSpanProcessors(
ConfigProperties properties, ChainingSpanProcessorRegisterer registerer) {

String enabledString =
properties.getString(ENABLED_OPTION, EnabledOptions.AUTO.toString()).toUpperCase();
String enabledDefault = EnabledOptions.AUTO.toString();
String unsupportedReason = JvmtiAccess.getSystemUnsupportedReason();
if (unsupportedReason != null) {
logger.log(
Level.FINE,
"Default value for {0} is false, because the system is unsupported: {1}",
new Object[] {ENABLED_OPTION, unsupportedReason});
enabledDefault = EnabledOptions.FALSE.toString();
}

String enabledString = properties.getString(ENABLED_OPTION, enabledDefault).toUpperCase();
EnabledOptions enabled = EnabledOptions.valueOf(enabledString);

if (enabled == EnabledOptions.FALSE) {
Expand Down

0 comments on commit ef59dc0

Please sign in to comment.