diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index db0990aa8fd..932f8fb60c3 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -1162,7 +1162,7 @@ private static void initializeCrashTracking(boolean delayed, boolean checkNative SEND_TELEMETRY, "Crashtracking failed to initialize. No additional details available."); } } catch (Throwable t) { - log.debug(SEND_TELEMETRY, "Unable to initialize crashtracking", t); + log.debug(SEND_TELEMETRY, "Unable to initialize crashtracking"); } } diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java index 5c2f739480a..dad46052637 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/Initializer.java @@ -103,7 +103,7 @@ public static boolean initialize(boolean forceJmx) { initializeOOMENotifier(access); return true; } catch (Throwable t) { - LOG.debug(SEND_TELEMETRY, "Failed to initialize crash tracking: " + t.getMessage(), t); + LOG.debug("Failed to initialize crash tracking: " + t.getMessage(), t); } return false; } diff --git a/dd-java-agent/agent-profiling/profiling-controller-openjdk/src/main/java/com/datadog/profiling/controller/openjdk/events/SmapEntryCache.java b/dd-java-agent/agent-profiling/profiling-controller-openjdk/src/main/java/com/datadog/profiling/controller/openjdk/events/SmapEntryCache.java index 411ed11cd53..4b03fff0396 100644 --- a/dd-java-agent/agent-profiling/profiling-controller-openjdk/src/main/java/com/datadog/profiling/controller/openjdk/events/SmapEntryCache.java +++ b/dd-java-agent/agent-profiling/profiling-controller-openjdk/src/main/java/com/datadog/profiling/controller/openjdk/events/SmapEntryCache.java @@ -1,7 +1,5 @@ package com.datadog.profiling.controller.openjdk.events; -import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; - import datadog.environment.JavaVirtualMachine; import de.thetaphi.forbiddenapis.SuppressForbidden; import java.io.BufferedReader; @@ -296,7 +294,7 @@ private static Map getAnnotatedRegions() { } return annotatedRegions; } catch (Exception e) { - log.debug(SEND_TELEMETRY, "Failed to get annotated regions", e); + log.debug("Failed to get annotated regions", e); } return Collections.emptyMap(); } @@ -311,9 +309,9 @@ private void collectEvents(List events) { } log.debug("Collected {} smap entry events.", events.size()); } catch (IOException e) { - log.debug(SEND_TELEMETRY, "Failed to read smap file", e); + log.debug("Failed to read smap file", e); } catch (Exception e) { - log.debug(SEND_TELEMETRY, "Failed to parse smap file", e); + log.debug("Failed to parse smap file", e); } } } diff --git a/dd-java-agent/agent-profiling/profiling-controller-openjdk/src/main/java/com/datadog/profiling/controller/openjdk/events/SmapEntryFactory.java b/dd-java-agent/agent-profiling/profiling-controller-openjdk/src/main/java/com/datadog/profiling/controller/openjdk/events/SmapEntryFactory.java index 9171730286a..68b3e063da6 100644 --- a/dd-java-agent/agent-profiling/profiling-controller-openjdk/src/main/java/com/datadog/profiling/controller/openjdk/events/SmapEntryFactory.java +++ b/dd-java-agent/agent-profiling/profiling-controller-openjdk/src/main/java/com/datadog/profiling/controller/openjdk/events/SmapEntryFactory.java @@ -1,9 +1,8 @@ package com.datadog.profiling.controller.openjdk.events; -import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; - import datadog.environment.JavaVirtualMachine; import datadog.environment.OperatingSystem; +import datadog.trace.api.profiling.ProfilerFlareLogger; import datadog.trace.bootstrap.instrumentation.jfr.JfrHelper; import java.lang.management.ManagementFactory; import java.time.Duration; @@ -62,9 +61,8 @@ public static void registerEvents() { log.debug("Smap entry events registered successfully"); } } catch (Exception e) { - log.debug( - SEND_TELEMETRY, - "Smap entry events could not be registered due to missing systemMap operation"); + ProfilerFlareLogger.getInstance() + .log("Smap entry events could not be registered due to missing systemMap operation", e); } } } diff --git a/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/EnvironmentChecker.java b/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/EnvironmentChecker.java new file mode 100644 index 00000000000..6d4a6e3019e --- /dev/null +++ b/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/EnvironmentChecker.java @@ -0,0 +1,265 @@ +package com.datadog.profiling.controller; + +import datadog.environment.JavaVirtualMachine; +import datadog.environment.OperatingSystem; +import datadog.environment.SystemProperties; +import de.thetaphi.forbiddenapis.SuppressForbidden; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; +import java.util.function.Supplier; +import java.util.jar.JarFile; + +public final class EnvironmentChecker { + private static void appendLine(String line, StringBuilder sb) { + sb.append(line).append(System.lineSeparator()); + } + + private static void appendLine(Supplier sbSupplier) { + sbSupplier.get().append(System.lineSeparator()); + } + + @SuppressForbidden + public static boolean checkEnvironment(String temp, StringBuilder sb) { + if (!JavaVirtualMachine.isJavaVersionAtLeast(8)) { + appendLine("Profiler requires Java 8 or newer", sb); + return false; + } + appendLine( + () -> + sb.append("Using Java version: ") + .append(JavaVirtualMachine.getRuntimeVersion()) + .append(" (") + .append(SystemProperties.getOrDefault("java.home", "unknown")) + .append(")")); + appendLine( + () -> + sb.append("Running as user: ") + .append(SystemProperties.getOrDefault("user.name", "unknown"))); + boolean result = false; + result |= checkJFR(sb); + result |= checkDdprof(sb); + if (!result) {; + appendLine("Profiler is not supported on this JVM.", sb); + return false; + } else { + appendLine("Profiler is supported on this JVM.", sb); + } + sb.append(System.lineSeparator()); + if (!checkTempLocation(temp, sb)) { + appendLine("Profiler will not work properly due to issues with temp directory location.", sb); + return false; + } else { + if (!temp.equals(SystemProperties.get("java.io.tmpdir"))) { + appendLine( + () -> + sb.append("! Make sure to add '-Ddd.profiling.tempdir=") + .append(temp) + .append("' to your JVM command line !")); + } + } + appendLine("Profiler is ready to be used.", sb); + return true; + } + + @SuppressForbidden + private static boolean checkJFR(StringBuilder sb) { + if (JavaVirtualMachine.isOracleJDK8()) { + appendLine( + "JFR is commercial feature in Oracle JDK 8. Make sure you have the right license.", sb); + return true; + } else if (JavaVirtualMachine.isJ9()) { + appendLine("JFR is not supported on J9 JVM.", sb); + return false; + } else { + appendLine( + () -> sb.append("JFR is supported on ").append(JavaVirtualMachine.getRuntimeVersion())); + return true; + } + } + + @SuppressForbidden + private static boolean checkDdprof(StringBuilder sb) { + if (!OperatingSystem.isLinux()) { + appendLine("Datadog profiler is only supported on Linux.", sb); + return false; + } else { + appendLine( + () -> + sb.append("Datadog profiler is supported on ") + .append(JavaVirtualMachine.getRuntimeVersion())); + return true; + } + } + + @SuppressForbidden + private static boolean checkTempLocation(String temp, StringBuilder sb) { + // Check if the temp directory is writable + if (temp == null || temp.isEmpty()) { + appendLine("Temp directory is not specified.", sb); + return false; + } + + appendLine(() -> sb.append("Checking temporary directory: ").append(temp)); + + Path base = Paths.get(temp); + if (!Files.exists(base)) { + appendLine(() -> sb.append("Temporary directory does not exist: ").append(base)); + return false; + } + Path target = base.resolve("dd-profiler").normalize(); + boolean rslt = true; + Set supportedViews = FileSystems.getDefault().supportedFileAttributeViews(); + boolean isPosix = supportedViews.contains("posix"); + try { + if (isPosix) { + Files.createDirectories( + target, + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + } else { + // non-posix, eg. Windows - let's rely on the created folders being world-writable + Files.createDirectories(target); + } + appendLine(() -> sb.append("Temporary directory is writable: ").append(target)); + rslt &= checkCreateTempFile(target, sb); + rslt &= checkLoadLibrary(target, sb); + } catch (Exception e) { + appendLine(() -> sb.append("Unable to create temp directory in location ").append(temp)); + if (isPosix) { + appendLine( + () -> + sb.append("Base dir: ") + .append(base) + .append(" [") + .append(getPermissionsStringSafe(base)) + .append("]")); + } + appendLine(() -> sb.append("Error: ").append(e)); + } finally { + if (Files.exists(target)) { + try { + Files.walkFileTree( + target, + new FileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException ignored) { + // should never happen + } + } + } + return rslt; + } + + private static String getPermissionsStringSafe(Path file) { + try { + return PosixFilePermissions.toString(Files.getPosixFilePermissions(file)); + } catch (IOException ignored) { + return ""; + } + } + + @SuppressForbidden + private static boolean checkCreateTempFile(Path target, StringBuilder sb) { + // create a file to check if the directory is writable + try { + appendLine(() -> sb.append("Attempting to create a test file in: ").append(target)); + Path testFile = target.resolve("testfile"); + Files.createFile(testFile); + appendLine(() -> sb.append("Test file created: ").append(testFile)); + return true; + } catch (Exception e) { + appendLine(() -> sb.append("Unable to create test file in temp directory ").append(target)); + appendLine(() -> sb.append("Error: ").append(e)); + } + return false; + } + + @SuppressForbidden + private static boolean checkLoadLibrary(Path target, StringBuilder sb) { + if (!OperatingSystem.isLinux()) { + // we are loading the native library only on linux + appendLine("Skipping native library check on non-linux platform", sb); + return true; + } + boolean rslt = true; + try { + rslt &= extractSoFromJar(target, sb); + if (rslt) { + Path libFile = target.resolve("libjavaProfiler.so"); + appendLine(() -> sb.append("Attempting to load native library from: ").append(libFile)); + System.load(libFile.toString()); + appendLine("Native library loaded successfully", sb); + } + return true; + } catch (Throwable t) { + appendLine( + () -> sb.append("Unable to load native library in temp directory ").append(target)); + appendLine(() -> sb.append("Error: ").append(t)); + return false; + } + } + + @SuppressForbidden + private static boolean extractSoFromJar(Path target, StringBuilder sb) throws Exception { + URL jarUrl = EnvironmentChecker.class.getProtectionDomain().getCodeSource().getLocation(); + try (JarFile jarFile = new JarFile(new File(jarUrl.toURI()))) { + return jarFile.stream() + .filter(e -> e.getName().contains("libjavaProfiler.so")) + .filter( + e -> + e.getName() + .contains(OperatingSystem.isAarch64() ? "/linux-arm64/" : "/linux-x64/") + && (!OperatingSystem.isMusl() || e.getName().contains("-musl"))) + .findFirst() + .map( + e -> { + try { + Path soFile = target.resolve("libjavaProfiler.so"); + Files.createDirectories(soFile.getParent()); + Files.copy(jarFile.getInputStream(e), soFile); + appendLine(() -> sb.append("Native library extracted to: ").append(soFile)); + return true; + } catch (Throwable t) { + appendLine("Failed to extract or load native library", sb); + appendLine(() -> sb.append("Error: ").append(t)); + } + return false; + }) + .orElse(Boolean.FALSE); + } + } +} diff --git a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilerFlareReporter.java b/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilerFlareReporter.java similarity index 97% rename from dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilerFlareReporter.java rename to dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilerFlareReporter.java index 1ae6062673e..3cd893e372f 100644 --- a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilerFlareReporter.java +++ b/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilerFlareReporter.java @@ -1,6 +1,5 @@ -package com.datadog.profiling.agent; +package com.datadog.profiling.controller; -import com.datadog.profiling.controller.ProfilingSupport; import datadog.trace.api.Config; import datadog.trace.api.config.ProfilingConfig; import datadog.trace.api.flare.TracerFlare; @@ -15,14 +14,14 @@ public final class ProfilerFlareReporter implements TracerFlare.Reporter { private static final ProfilerFlareReporter INSTANCE = new ProfilerFlareReporter(); - private static Exception profilerInitializationException; + private volatile Exception profilerInitializationException; public static void register() { TracerFlare.addReporter(INSTANCE); } public static void reportInitializationException(Exception e) { - profilerInitializationException = e; + INSTANCE.profilerInitializationException = e; } @Override @@ -40,6 +39,12 @@ public void addReportToFlare(ZipOutputStream zip) throws IOException { // no-op, ignore if we can't read the template override file } } + + StringBuilder envCheck = new StringBuilder(); + String tempDir = ConfigProvider.getInstance().getString(ProfilingConfig.PROFILING_TEMP_DIR); + EnvironmentChecker.checkEnvironment( + tempDir != null ? tempDir : System.getProperty("java.io.tmpdir"), envCheck); + TracerFlare.addText(zip, "profiler_env.txt", envCheck.toString()); } private String getProfilerConfig() { diff --git a/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilerSettingsSupport.java b/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilerSettingsSupport.java index d64fe983cc6..d2a1d1e81e7 100644 --- a/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilerSettingsSupport.java +++ b/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilerSettingsSupport.java @@ -189,10 +189,6 @@ protected ProfilerSettingsSupport( // usually set via DD_INSTRUMENTATION_INSTALL_TYPE env var configProvider.getString("instrumentation.install.type"); this.profilerActivationSetting = getProfilerActivation(configProvider); - - logger.debug( - SEND_TELEMETRY, - "Profiler settings: " + this); // telemetry receiver does not recognize formatting } private static int getStackDepth() { @@ -212,8 +208,7 @@ private static int getStackDepth() { try { return Integer.parseInt(value.substring(start, end)); } catch (NumberFormatException e) { - logger.debug( - SEND_TELEMETRY, "Failed to parse stack depth from JFR options: {}", value, e); + logger.debug(SEND_TELEMETRY, "Failed to parse stack depth from JFR options"); } } } diff --git a/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilingSystem.java b/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilingSystem.java index d1d7bfd8c99..7f57b356d99 100644 --- a/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilingSystem.java +++ b/dd-java-agent/agent-profiling/profiling-controller/src/main/java/com/datadog/profiling/controller/ProfilingSystem.java @@ -22,6 +22,7 @@ import static datadog.trace.util.AgentThreadFactory.AgentThread.PROFILER_RECORDING_SCHEDULER; import datadog.environment.JavaVirtualMachine; +import datadog.trace.api.profiling.ProfilerFlareLogger; import datadog.trace.api.profiling.ProfilingSnapshot; import datadog.trace.api.profiling.RecordingData; import datadog.trace.api.profiling.RecordingDataListener; @@ -163,15 +164,16 @@ private void startProfilingRecording() { TimeUnit.MILLISECONDS); started = true; } catch (UnsupportedEnvironmentException unsupported) { - log.warn( - SEND_TELEMETRY, - "Datadog Profiling was enabled on an unsupported JVM, will not profile application. " - + "(OS: {}, JVM: lang={}, runtime={}, vendor={}) See {} for more details about supported JVMs.", - isLinux() ? "Linux" : isWindows() ? "Windows" : isMacOs() ? "MacOS" : "Other", - JavaVirtualMachine.getLangVersion(), - JavaVirtualMachine.getRuntimeVersion(), - JavaVirtualMachine.getRuntimeVendor(), - "https://docs.datadoghq.com/profiler/enabling/java/?tab=commandarguments#requirements"); + ProfilerFlareLogger.getInstance() + .log( + "Datadog Profiling was enabled on an unsupported JVM, will not profile application. " + + "(OS: {}, JVM: lang={}, runtime={}, vendor={}) See {} for more details about supported JVMs.", + isLinux() ? "Linux" : isWindows() ? "Windows" : isMacOs() ? "MacOS" : "Other", + JavaVirtualMachine.getLangVersion(), + JavaVirtualMachine.getRuntimeVersion(), + JavaVirtualMachine.getRuntimeVendor(), + "https://docs.datadoghq.com/profiler/enabling/java/?tab=commandarguments#requirements", + unsupported); } catch (Throwable t) { if (t instanceof RuntimeException) { // Possibly a wrapped exception related to Oracle JDK 8 JFR MX beans @@ -180,9 +182,10 @@ private void startProfilingRecording() { String msg = inspecting.getMessage(); if (msg != null && msg.contains("com.oracle.jrockit:type=FlightRecorder")) { // Yes, the commercial JFR is not enabled - log.warn( - SEND_TELEMETRY, - "You're running Oracle JDK 8. Datadog Continuous Profiler for Java depends on Java Flight Recorder, which requires a paid license in Oracle JDK 8. If you have one, please add the following `java` command line args: ‘-XX:+UnlockCommercialFeatures -XX:+FlightRecorder’. Alternatively, you can use a different Java 8 distribution like OpenJDK, where Java Flight Recorder is free."); + String logMsg = + "You're running Oracle JDK 8. Datadog Continuous Profiler for Java depends on Java Flight Recorder, which requires a paid license in Oracle JDK 8. If you have one, please add the following `java` command line args: ‘-XX:+UnlockCommercialFeatures -XX:+FlightRecorder’. Alternatively, you can use a different Java 8 distribution like OpenJDK, where Java Flight Recorder is free."; + ProfilerFlareLogger.getInstance().log(logMsg, t); + log.warn(logMsg); // Do not log the underlying exception t = null; break; @@ -192,9 +195,10 @@ private void startProfilingRecording() { } if (t != null) { if (t instanceof IllegalStateException && "Shutdown in progress".equals(t.getMessage())) { - log.debug("Shutdown in progress, cannot start profiling"); + ProfilerFlareLogger.getInstance().log("Shutdown in progress, cannot start profiling"); } else { - log.error(SEND_TELEMETRY, "Fatal exception during profiling startup", t); + ProfilerFlareLogger.getInstance().log("Failed to start profiling", t); + throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t); } } diff --git a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/CompositeController.java b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/CompositeController.java index e44a8428a28..0a7c100341b 100644 --- a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/CompositeController.java +++ b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/CompositeController.java @@ -18,6 +18,7 @@ import datadog.trace.api.Config; import datadog.trace.api.Platform; import datadog.trace.api.config.ProfilingConfig; +import datadog.trace.api.profiling.ProfilerFlareLogger; import datadog.trace.api.profiling.ProfilingSnapshot; import datadog.trace.api.profiling.RecordingData; import datadog.trace.api.profiling.RecordingInputStream; @@ -156,7 +157,7 @@ public static Controller build(ConfigProvider provider, ControllerContext contex Class.forName("com.oracle.jrockit.jfr.Producer"); controllers.add(OracleJdkController.instance(provider)); } catch (Throwable t) { - log.debug(SEND_TELEMETRY, "Failed to load oracle profiler: {}", t.getMessage(), t); + ProfilerFlareLogger.getInstance().log("Failed to load oracle profiler", t); } } if (!isOracleJDK8) { @@ -164,15 +165,14 @@ public static Controller build(ConfigProvider provider, ControllerContext contex if (Platform.hasJfr()) { controllers.add(OpenJdkController.instance(provider)); } else { - log.debug( - SEND_TELEMETRY, - "JFR is not available on this platform: " - + OperatingSystem.current() - + ", " - + Arch.current()); + ProfilerFlareLogger.getInstance() + .log( + "JFR is not available on this platform: {}, {}", + OperatingSystem.current(), + Arch.current()); } } catch (Throwable t) { - log.debug(SEND_TELEMETRY, "Failed to load openjdk profiler: " + t.getMessage(), t); + ProfilerFlareLogger.getInstance().log("Failed to load openjdk profiler", t); } } } @@ -188,20 +188,12 @@ public static Controller build(ConfigProvider provider, ControllerContext contex context.setDatadogProfilerUnavailableReason(rootCause.getMessage()); OperatingSystem os = OperatingSystem.current(); if (os != OperatingSystem.linux) { - log.debug(SEND_TELEMETRY, "Datadog profiler only supported on Linux", rootCause); - } else if (!log.isDebugEnabled()) { - log.warn( - "failed to instantiate Datadog profiler on {} {} because: {}", - os, - Arch.current(), - rootCause.getMessage()); + ProfilerFlareLogger.getInstance() + .log("Datadog profiler only supported on Linux", rootCause); } else { - log.debug( - SEND_TELEMETRY, - "failed to instantiate Datadog profiler on {} {}", - os, - Arch.current(), - rootCause); + ProfilerFlareLogger.getInstance() + .log( + "Failed to instantiate Datadog profiler on {} {}", os, Arch.current(), rootCause); } } } else { diff --git a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java index e0ef1645509..c73b618edb8 100644 --- a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java +++ b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java @@ -10,6 +10,7 @@ import com.datadog.profiling.controller.ConfigurationException; import com.datadog.profiling.controller.Controller; import com.datadog.profiling.controller.ControllerContext; +import com.datadog.profiling.controller.ProfilerFlareReporter; import com.datadog.profiling.controller.ProfilingSystem; import com.datadog.profiling.controller.UnsupportedEnvironmentException; import com.datadog.profiling.controller.jfr.JFRAccess; @@ -18,6 +19,7 @@ import datadog.trace.api.Config; import datadog.trace.api.Platform; import datadog.trace.api.config.ProfilingConfig; +import datadog.trace.api.profiling.ProfilerFlareLogger; import datadog.trace.api.profiling.RecordingData; import datadog.trace.api.profiling.RecordingDataListener; import datadog.trace.api.profiling.RecordingType; @@ -171,16 +173,8 @@ public static synchronized boolean run(final boolean earlyStart, Instrumentation } catch (final IllegalStateException ex) { // The JVM is already shutting down. } - } catch (final UnsupportedEnvironmentException e) { - log.warn(e.getMessage()); - // no need to send telemetry for this aggregate message - // a detailed telemetry message has been sent from the attempts to enable the controllers - // ----------------------------------------------------------------------------------------- - // but we do want to report this within the profiler flare - ProfilerFlareReporter.reportInitializationException(e); - } catch (final ConfigurationException e) { - log.warn("Failed to initialize profiling agent! {}", e.getMessage()); - log.debug(SEND_TELEMETRY, "Failed to initialize profiling agent!", e); + } catch (final UnsupportedEnvironmentException | ConfigurationException e) { + ProfilerFlareLogger.getInstance().log("Failed to initialize profiling agent!", e); ProfilerFlareReporter.reportInitializationException(e); } } diff --git a/dd-java-agent/agent-tooling/build.gradle b/dd-java-agent/agent-tooling/build.gradle index 7c4b48f35e9..5f8dbf28c66 100644 --- a/dd-java-agent/agent-tooling/build.gradle +++ b/dd-java-agent/agent-tooling/build.gradle @@ -43,6 +43,7 @@ dependencies { } compileOnly project(':dd-java-agent:agent-jmxfetch') compileOnly project(':dd-java-agent:agent-profiling') + compileOnly project(':dd-java-agent:agent-profiling:profiling-controller') api group: 'com.blogspot.mydailyjava', name: 'weak-lock-free', version: '0.17' api libs.bytebuddy api libs.bytebuddyagent diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/profiler/EnvironmentChecker.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/profiler/EnvironmentChecker.java index 8436ea435b1..5eb2d73c426 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/profiler/EnvironmentChecker.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/profiler/EnvironmentChecker.java @@ -1,241 +1,15 @@ package datadog.trace.agent.tooling.profiler; -import datadog.environment.JavaVirtualMachine; -import datadog.environment.OperatingSystem; -import datadog.environment.SystemProperties; import de.thetaphi.forbiddenapis.SuppressForbidden; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.FileSystems; -import java.nio.file.FileVisitResult; -import java.nio.file.FileVisitor; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.Set; -import java.util.jar.JarFile; public final class EnvironmentChecker { @SuppressForbidden public static boolean checkEnvironment(String temp) { - if (!JavaVirtualMachine.isJavaVersionAtLeast(8)) { - System.out.println("Profiler requires Java 8 or newer"); - return false; - } - System.out.println( - "Using Java version: " - + JavaVirtualMachine.getRuntimeVersion() - + " (" - + SystemProperties.getOrDefault("java.home", "unknown") - + ")"); - System.out.println("Running as user: " + SystemProperties.getOrDefault("user.name", "unknown")); - boolean result = false; - result |= checkJFR(); - result |= checkDdprof(); - if (!result) {; - System.out.println("Profiler is not supported on this JVM."); - return false; - } else { - System.out.println("Profiler is supported on this JVM."); - } - System.out.println(); - if (!checkTempLocation(temp)) { - System.out.println( - "Profiler will not work properly due to issues with temp directory location."); - return false; - } else { - if (!temp.equals(SystemProperties.get("java.io.tmpdir"))) { - System.out.println( - "! Make sure to add '-Ddd.profiling.tempdir=" + temp + "' to your JVM command line !"); - } - } - System.out.println("Profiler is ready to be used."); - return true; - } - - @SuppressForbidden - private static boolean checkJFR() { - if (JavaVirtualMachine.isOracleJDK8()) { - System.out.println( - "JFR is commercial feature in Oracle JDK 8. Make sure you have the right license."); - return true; - } else if (JavaVirtualMachine.isJ9()) { - System.out.println("JFR is not supported on J9 JVM."); - return false; - } else { - System.out.println("JFR is supported on " + JavaVirtualMachine.getRuntimeVersion()); - return true; - } - } - - @SuppressForbidden - private static boolean checkDdprof() { - if (!OperatingSystem.isLinux()) { - System.out.println("Datadog profiler is only supported on Linux."); - return false; - } else { - System.out.println( - "Datadog profiler is supported on " + JavaVirtualMachine.getRuntimeVersion()); - return true; - } - } - - @SuppressForbidden - private static boolean checkTempLocation(String temp) { - // Check if the temp directory is writable - if (temp == null || temp.isEmpty()) { - System.out.println("Temp directory is not specified."); - return false; - } - - System.out.println("Checking temporary directory: " + temp); - - Path base = Paths.get(temp); - if (!Files.exists(base)) { - System.out.println("Temporary directory does not exist: " + base); - return false; - } - Path target = base.resolve("dd-profiler").normalize(); - boolean rslt = true; - Set supportedViews = FileSystems.getDefault().supportedFileAttributeViews(); - boolean isPosix = supportedViews.contains("posix"); - try { - if (isPosix) { - Files.createDirectories( - target, - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); - } else { - // non-posix, eg. Windows - let's rely on the created folders being world-writable - Files.createDirectories(target); - } - System.out.println("Temporary directory is writable: " + target); - rslt &= checkCreateTempFile(target); - rslt &= checkLoadLibrary(target); - } catch (Exception e) { - System.out.println("Unable to create temp directory in location " + temp); - if (isPosix) { - try { - System.out.println( - "Base dir: " - + base - + " [" - + PosixFilePermissions.toString(Files.getPosixFilePermissions(base)) - + "]"); - } catch (IOException ignored) { - // never happens - } - } - System.out.println("Error: " + e); - } finally { - if (Files.exists(target)) { - try { - Files.walkFileTree( - target, - new FileVisitor() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) - throws IOException { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFileFailed(Path file, IOException exc) - throws IOException { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) - throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException ignored) { - // should never happen - } - } - } + StringBuilder builder = new StringBuilder(); + // forward the functionality to the core profiling env checker class + boolean rslt = + com.datadog.profiling.controller.EnvironmentChecker.checkEnvironment(temp, builder); + System.out.println(builder); return rslt; } - - @SuppressForbidden - private static boolean checkCreateTempFile(Path target) { - // create a file to check if the directory is writable - try { - System.out.println("Attempting to create a test file in: " + target); - Path testFile = target.resolve("testfile"); - Files.createFile(testFile); - System.out.println("Test file created: " + testFile); - return true; - } catch (Exception e) { - System.out.println("Unable to create test file in temp directory " + target); - System.out.println("Error: " + e); - } - return false; - } - - @SuppressForbidden - private static boolean checkLoadLibrary(Path target) { - if (!OperatingSystem.isLinux()) { - // we are loading the native library only on linux - System.out.println("Skipping native library check on non-linux platform"); - return true; - } - boolean rslt = true; - try { - rslt &= extractSoFromJar(target); - if (rslt) { - Path libFile = target.resolve("libjavaProfiler.so"); - System.out.println("Attempting to load native library from: " + libFile); - System.load(libFile.toString()); - System.out.println("Native library loaded successfully"); - } - return true; - } catch (Throwable t) { - System.out.println("Unable to load native library in temp directory " + target); - System.out.println("Error: " + t); - return false; - } - } - - @SuppressForbidden - private static boolean extractSoFromJar(Path target) throws Exception { - URL jarUrl = EnvironmentChecker.class.getProtectionDomain().getCodeSource().getLocation(); - try (JarFile jarFile = new JarFile(new File(jarUrl.toURI()))) { - return jarFile.stream() - .filter(e -> e.getName().contains("libjavaProfiler.so")) - .filter( - e -> - e.getName() - .contains(OperatingSystem.isAarch64() ? "/linux-arm64/" : "/linux-x64/") - && (!OperatingSystem.isMusl() || e.getName().contains("-musl"))) - .findFirst() - .map( - e -> { - try { - Path soFile = target.resolve("libjavaProfiler.so"); - Files.createDirectories(soFile.getParent()); - Files.copy(jarFile.getInputStream(e), soFile); - System.out.println("Native library extracted to: " + soFile); - return true; - } catch (Throwable t) { - System.out.println("Failed to extract or load native library"); - System.out.println("Error: " + t); - } - return false; - }) - .orElse(Boolean.FALSE); - } - } } diff --git a/dd-smoke-tests/tracer-flare/src/test/groovy/datadog/smoketest/TracerFlareSmokeTest.groovy b/dd-smoke-tests/tracer-flare/src/test/groovy/datadog/smoketest/TracerFlareSmokeTest.groovy index 613f7417048..fb3ca590f02 100644 --- a/dd-smoke-tests/tracer-flare/src/test/groovy/datadog/smoketest/TracerFlareSmokeTest.groovy +++ b/dd-smoke-tests/tracer-flare/src/test/groovy/datadog/smoketest/TracerFlareSmokeTest.groovy @@ -131,12 +131,14 @@ class TracerFlareSmokeTest extends AbstractSmokeTest { // Only if there were errors "pending_traces.txt", // Only if there were traces pending transmission - "profiling_template_override.jfp" + "profiling_template_override.jfp", // Only if template override is configured + "profiler_log.txt" + // Only if there are any profiler issues reported ] as Set // Profiling-related files - private static final PROFILING_FILES = ["profiler_config.txt", + private static final PROFILING_FILES = ["profiler_config.txt", "profiler_env.txt" // Only if profiling is enabled ] as Set diff --git a/internal-api/src/main/java/datadog/trace/api/profiling/ProfilerFlareLogger.java b/internal-api/src/main/java/datadog/trace/api/profiling/ProfilerFlareLogger.java new file mode 100644 index 00000000000..e6d917296d9 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/profiling/ProfilerFlareLogger.java @@ -0,0 +1,95 @@ +package datadog.trace.api.profiling; + +import datadog.trace.api.flare.TracerFlare; +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipOutputStream; +import org.slf4j.helpers.FormattingTuple; +import org.slf4j.helpers.MessageFormatter; + +public final class ProfilerFlareLogger implements TracerFlare.Reporter { + private static final class Singleton { + private static final ProfilerFlareLogger INSTANCE = new ProfilerFlareLogger(); + } + + private final int REPORT_CAPACITY = 2 * 1024 * 1024; // 2MiB max in profiler reports + private final List flareReportLines = new ArrayList<>(); + private int usedReportCapacity = 0; + + // @VisibleForTesting + ProfilerFlareLogger() { + TracerFlare.addReporter(this); + } + + public static ProfilerFlareLogger getInstance() { + return Singleton.INSTANCE; + } + + /** + * Logs the message in slf4j format to the flare log storage.
+ * + * @param msgFormat the message format in slf4j style + * @param args the arguments for the message format + * @return Returns {@literal true} if the message was stored for flare, {@literal false} otherwise + */ + public boolean log(String msgFormat, Object... args) { + FormattingTuple ft = MessageFormatter.arrayFormat(msgFormat, args); + StringBuilder sb = + new StringBuilder(Instant.now().atZone(ZoneOffset.UTC).toString()) + .append('\t') + .append(ft.getMessage()) + .append('\n'); + if (ft.getThrowable() != null) { + sb.append(ft.getThrowable()); + } + synchronized (flareReportLines) { + if (usedReportCapacity + sb.length() < REPORT_CAPACITY) { + flareReportLines.add(sb.toString()); + usedReportCapacity += sb.length(); + return true; + } + } + return false; + } + + @Override + public void addReportToFlare(ZipOutputStream zip) throws IOException { + synchronized (flareReportLines) { + if (!flareReportLines.isEmpty()) { + TracerFlare.addText(zip, "profiler_log.txt", String.join("\n", flareReportLines)); + } + } + } + + @Override + public void cleanupAfterFlare() { + cleanup(); + } + + // @VisibleForTesting + int getUsedReportCapacity() { + return usedReportCapacity; + } + + // @VisibleForTesting + int getMaxReportCapacity() { + return REPORT_CAPACITY; + } + + // @VisibleForTesting + int linesSize() { + synchronized (flareReportLines) { + return flareReportLines.size(); + } + } + + void cleanup() { + synchronized (flareReportLines) { + flareReportLines.clear(); + usedReportCapacity = 0; + } + } +} diff --git a/internal-api/src/main/java/datadog/trace/util/TempLocationManager.java b/internal-api/src/main/java/datadog/trace/util/TempLocationManager.java index 951e4e124d9..825aa74db56 100644 --- a/internal-api/src/main/java/datadog/trace/util/TempLocationManager.java +++ b/internal-api/src/main/java/datadog/trace/util/TempLocationManager.java @@ -1,10 +1,9 @@ package datadog.trace.util; -import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; - import datadog.environment.EnvironmentVariables; import datadog.environment.SystemProperties; import datadog.trace.api.config.ProfilingConfig; +import datadog.trace.api.profiling.ProfilerFlareLogger; import datadog.trace.bootstrap.config.provider.ConfigProvider; import java.io.IOException; import java.nio.file.FileSystems; @@ -23,7 +22,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Pattern; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,8 +34,6 @@ */ public final class TempLocationManager { private static final Logger log = LoggerFactory.getLogger(TempLocationManager.class); - private static final Pattern JFR_DIR_PATTERN = - Pattern.compile("\\d{4}_\\d{2}_\\d{2}_\\d{2}_\\d{2}_\\d{2}_\\d{6}"); private static final String TEMPDIR_PREFIX = "pid_"; private static final class SingletonHolder { @@ -277,12 +273,8 @@ private TempLocationManager() { configProvider.getString( ProfilingConfig.PROFILING_TEMP_DIR, ProfilingConfig.PROFILING_TEMP_DIR_DEFAULT)); if (!Files.exists(configuredTempDir)) { - log.warn( - SEND_TELEMETRY, - "Base temp directory, as defined in '" - + ProfilingConfig.PROFILING_TEMP_DIR - + "' does not exist: {}", - configuredTempDir); + ProfilerFlareLogger.getInstance() + .log("Base temp directory, as defined in '{}' does not exist.", configuredTempDir); throw new IllegalStateException( "Base temp directory, as defined in '" + ProfilingConfig.PROFILING_TEMP_DIR @@ -391,11 +383,7 @@ boolean cleanup(long timeout, TimeUnit unit) { Files.walkFileTree(baseTempDir, visitor); return !visitor.isTerminated(); } catch (IOException e) { - if (log.isDebugEnabled()) { - log.warn("Unable to cleanup temp location {}", baseTempDir, e); - } else { - log.warn("Unable to cleanup temp location {}", baseTempDir); - } + log.debug("Unable to cleanup temp location {}", baseTempDir, e); } return false; } @@ -433,7 +421,6 @@ private void createTempDir(Path tempDir) { Files.createDirectories(tempDir); } } catch (IOException e) { - log.error("Failed to create temp directory {}", tempDir, e); // if on a posix fs, let's check the expected permissions // we will find the first offender not having the expected permissions and fail the check if (isPosixFs) { @@ -480,20 +467,22 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) Path failedDir = failed.get(); if (failedDir != null) { - msg += - " (offender: " - + failedDir - + ", permissions: " - + PosixFilePermissions.toString(Files.getPosixFilePermissions(failedDir)) - + ")"; - log.warn(SEND_TELEMETRY, msg, e); + ProfilerFlareLogger.getInstance() + .log( + "Failed to create temp directory: {}, offender: {}, permissions: {}", + msg, + failedDir, + PosixFilePermissions.toString(Files.getPosixFilePermissions(failedDir)), + e); + } else { + ProfilerFlareLogger.getInstance().log(msg); } } catch (IOException ignored) { // should not happen, but let's ignore it anyway } throw new IllegalStateException(msg, e); } else { - log.warn(SEND_TELEMETRY, msg, e); + ProfilerFlareLogger.getInstance().log(msg, e); throw new IllegalStateException(msg, e); } } diff --git a/internal-api/src/test/java/datadog/trace/api/profiling/ProfilerFlareLoggerTest.java b/internal-api/src/test/java/datadog/trace/api/profiling/ProfilerFlareLoggerTest.java new file mode 100644 index 00000000000..5e57529e51f --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/profiling/ProfilerFlareLoggerTest.java @@ -0,0 +1,392 @@ +package datadog.trace.api.profiling; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import datadog.trace.api.flare.TracerFlare; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import java.util.zip.ZipOutputStream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; + +class ProfilerFlareLoggerTest { + + private ProfilerFlareLogger logger; + + @BeforeEach + void setUp() throws Exception { + logger = ProfilerFlareLogger.getInstance(); + } + + @AfterEach + void tearDown() throws Exception { + logger.cleanup(); + } + + // Singleton Pattern Tests + @Test + void testSingletonConsistency() { + ProfilerFlareLogger instance1 = ProfilerFlareLogger.getInstance(); + ProfilerFlareLogger instance2 = ProfilerFlareLogger.getInstance(); + + assertSame(instance1, instance2); + } + + @Test + void testSingletonThreadSafety() throws Exception { + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + + for (int i = 0; i < threadCount; i++) { + futures.add(executor.submit(ProfilerFlareLogger::getInstance)); + } + + ProfilerFlareLogger firstInstance = futures.get(0).get(); + for (Future future : futures) { + assertSame(firstInstance, future.get()); + } + + executor.shutdown(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + @Test + void testTracerFlareReporterRegistration() { + try (MockedStatic mockedStatic = mockStatic(TracerFlare.class)) { + ProfilerFlareLogger loggerInstance = new ProfilerFlareLogger(); + mockedStatic.verify(() -> TracerFlare.addReporter(loggerInstance)); + } + } + + // Logging Functionality Tests + @Test + void testBasicLogging() { + assertTrue(logger.log("Test message")); + } + + @Test + void testLoggingWithArguments() { + assertTrue(logger.log("Test message with {} and {}", "arg1", 42)); + } + + @Test + void testLoggingWithException() { + Exception testException = new RuntimeException("Test exception"); + assertTrue(logger.log("Error occurred: {}", "details", testException)); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "Simple message", "Message with special chars: !@#$%^&*()"}) + void testLoggingVariousMessages(String message) { + assertTrue(logger.log(message)); + } + + @Test + void testLoggingNullMessage() { + assertTrue(logger.log(null)); + } + + @Test + void testLoggingNullArguments() { + assertTrue(logger.log("Message with null: {}", (Object) null)); + } + + @ParameterizedTest + @MethodSource("provideLogMessageFormats") + void testSLF4JFormatting(String format, Object[] args, String expectedSubstring) { + assertTrue(logger.log(format, args)); + // Additional verification could be added if we can access the logged content + } + + private static Stream provideLogMessageFormats() { + return Stream.of( + Arguments.of("Simple message", new Object[]{}, "Simple message"), + Arguments.of("Message with {}", new Object[]{"placeholder"}, "placeholder"), + Arguments.of("Multiple {} and {}", new Object[]{"first", "second"}, "first"), + Arguments.of("Number: {}", new Object[]{123}, "123"), + Arguments.of("Boolean: {}", new Object[]{true}, "true") + ); + } + + // Capacity Management Tests + @Test + void testCapacityLimitEnforcement() throws Exception { + int capacity = logger.getMaxReportCapacity(); + + assertEquals(2 * 1024 * 1024, capacity); // 2MiB + } + + @Test + void testLoggingWithinCapacity() { + for (int i = 0; i < 100; i++) { + assertTrue(logger.log("Message {}", i)); + } + } + + @Test + void testCapacityRejection() { + // Fill up capacity with large messages + StringBuilder largeMessage = new StringBuilder(); + for (int i = 0; i < 10000; i++) { + largeMessage.append("Large message content "); + } + + int iterations = logger.getMaxReportCapacity() / largeMessage.length(); + String message = largeMessage.toString(); + + // We must accept all messages until capacity + for (int i = 0; i < iterations; i++) { + assertTrue(logger.log(message)); + } + + // Next message should be rejected + assertFalse(logger.log(message)); + } + + @Test + void testCapacityAccuracy() throws Exception { + String testMessage = "Test message"; + logger.log(testMessage); + + int usedCapacity = logger.getUsedReportCapacity(); + assertTrue(usedCapacity > testMessage.length()); // Should include timestamp and formatting + } + + // TracerFlare Integration Tests + @Test + void testAddReportToFlareWithEmptyLogs() throws Exception { + ZipOutputStream mockZip = mock(ZipOutputStream.class); + + logger.addReportToFlare(mockZip); + + // Should not add any entries for empty logs + verify(mockZip, never()).putNextEntry(any()); + } + + @Test + void testAddReportToFlareWithLogs() throws Exception { + ZipOutputStream mockZip = mock(ZipOutputStream.class); + + logger.log("Test message 1"); + logger.log("Test message 2"); + + try (MockedStatic mockedStatic = mockStatic(TracerFlare.class)) { + logger.addReportToFlare(mockZip); + mockedStatic.verify(() -> TracerFlare.addText(eq(mockZip), eq("profiler_log.txt"), anyString())); + } + } + + @Test + void testAddReportToFlareIOException() throws Exception { + ZipOutputStream mockZip = mock(ZipOutputStream.class); + logger.log("Test message"); + + try (MockedStatic mockedStatic = mockStatic(TracerFlare.class)) { + mockedStatic.when(() -> TracerFlare.addText(any(), any(), any())) + .thenThrow(new IOException("Test IO exception")); + + assertThrows(IOException.class, () -> logger.addReportToFlare(mockZip)); + } + } + + @Test + void testFlareContentFormat() throws Exception { + logger.log("Message 1"); + logger.log("Message 2 with {}", "arg"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(baos); + + try (MockedStatic mockedStatic = mockStatic(TracerFlare.class)) { + mockedStatic.when(() -> TracerFlare.addText(any(), any(), any())).then(invocation -> { + String content = invocation.getArgument(2); + assertTrue(content.contains("Message 1")); + assertTrue(content.contains("Message 2 with arg")); + assertTrue(content.contains("\n")); // Should have newlines between messages + return null; + }); + + logger.addReportToFlare(zip); + } + } + + // Cleanup Tests + @Test + void testCleanup() throws Exception { + logger.log("Test message 1"); + logger.log("Test message 2"); + + logger.cleanup(); + + assertEquals(0, logger.linesSize()); + assertEquals(0, logger.getUsedReportCapacity()); + } + + @Test + void testLoggingAfterCleanup() { + logger.log("Before cleanup"); + logger.cleanup(); + + assertTrue(logger.log("After cleanup")); + } + + // Thread Safety Tests + @Test + void testConcurrentLogging() throws Exception { + int threadCount = 20; + int messagesPerThread = 50; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(threadCount); + AtomicInteger successCount = new AtomicInteger(0); + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + executor.submit(() -> { + try { + startLatch.await(); + for (int j = 0; j < messagesPerThread; j++) { + if (logger.log("Thread {} message {}", threadId, j)) { + successCount.incrementAndGet(); + } + } + } catch (Exception e) { + fail("Exception in thread: " + e.getMessage()); + } finally { + doneLatch.countDown(); + } + }); + } + + startLatch.countDown(); + assertTrue(doneLatch.await(10, TimeUnit.SECONDS)); + + // At least some messages should succeed + assertTrue(successCount.get() > 0); + + executor.shutdown(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + @Test + void testConcurrentCleanupAndLogging() throws Exception { + int iterations = 100; + ExecutorService executor = Executors.newFixedThreadPool(3); + + for (int i = 0; i < iterations; i++) { + CountDownLatch latch = new CountDownLatch(2); + + // Logger thread + executor.submit(() -> { + logger.log("Concurrent message"); + latch.countDown(); + }); + + // Cleanup thread + executor.submit(() -> { + logger.cleanup(); + latch.countDown(); + }); + + assertTrue(latch.await(1, TimeUnit.SECONDS)); + } + + executor.shutdown(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + // Performance and Edge Case Tests + @Test + void testLargeMessageHandling() { + StringBuilder largeMessage = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + largeMessage.append("This is a large message with lots of content. "); + } + + assertTrue(logger.log(largeMessage.toString())); + } + + @Test + void testManySmallMessages() { + for (int i = 0; i < 10000; i++) { + if (!logger.log("Message {}", i)) { + // Hit capacity limit, which is expected + break; + } + } + // Test should complete without throwing exceptions + } + + @Test + void testTimestampFormatting() { + // Log a message and verify it doesn't throw exceptions during timestamp formatting + assertDoesNotThrow(() -> logger.log("Timestamp test")); + } + + @Test + void testExceptionWithNullMessage() { + Exception testException = new RuntimeException(); + assertTrue(logger.log("Exception: {}", testException)); + } + + @Test + void testSpecialCharacterHandling() { + assertTrue(logger.log("Special chars: \n\t\r\\\"'")); + assertTrue(logger.log("Unicode: \u2603 \u2764 \u1F44D")); + } + + @Test + void testEmptyAndWhitespaceMessages() { + assertTrue(logger.log("")); + assertTrue(logger.log(" ")); + assertTrue(logger.log("\t\n\r")); + } + + @Test + void testMultipleExceptions() { + Exception cause = new IllegalStateException("Root cause"); + Exception wrapper = new RuntimeException("Wrapper", cause); + + assertTrue(logger.log("Nested exception: {}", wrapper)); + } + + @Test + void testLogAfterCapacityHit() { + // Fill to capacity + StringBuilder largeMessage = new StringBuilder(); + for (int i = 0; i < 50000; i++) { + largeMessage.append("Large message "); + } + + String message = largeMessage.toString(); + while (logger.log(message)) { + // Keep adding until capacity is hit + } + + // Verify subsequent messages are rejected + assertFalse(logger.log("Should be rejected: " + message)); + assertFalse(logger.log("Also rejected: " + message)); + + // After cleanup, should accept messages again + logger.cleanup(); + assertTrue(logger.log("After cleanup")); + } +}