diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java index 2e099582..f9d7931e 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java @@ -1,7 +1,9 @@ package com.teamscale.jacoco.agent; import com.teamscale.client.HttpUtils; +import com.teamscale.client.TeamscaleServer; import com.teamscale.jacoco.agent.configuration.AgentOptionReceiveException; +import com.teamscale.jacoco.agent.logging.LogToTeamscaleAppender; import com.teamscale.jacoco.agent.options.AgentOptionParseException; import com.teamscale.jacoco.agent.options.AgentOptions; import com.teamscale.jacoco.agent.options.AgentOptionsParser; @@ -11,6 +13,7 @@ import com.teamscale.jacoco.agent.options.TeamscalePropertiesUtils; import com.teamscale.jacoco.agent.testimpact.TestwiseCoverageAgent; import com.teamscale.jacoco.agent.upload.UploaderException; +import com.teamscale.jacoco.agent.upload.teamscale.TeamscaleConfig; import com.teamscale.jacoco.agent.util.AgentUtils; import com.teamscale.jacoco.agent.logging.DebugLogDirectoryPropertyDefiner; import com.teamscale.jacoco.agent.logging.LogDirectoryPropertyDefiner; @@ -31,6 +34,8 @@ import java.util.List; import java.util.Optional; +import static com.teamscale.jacoco.agent.logging.LoggingUtils.getLoggerContext; + /** Container class for the premain entry point for the agent. */ public class PreMain { @@ -149,6 +154,10 @@ private static void initializeLogging(AgentOptions agentOptions, DelayedLogger l loggingResources = LoggingUtils.initializeLogging(agentOptions.getLoggingConfig()); logger.info("Logging to " + new LogDirectoryPropertyDefiner().getPropertyValue()); } + + if (agentOptions.getTeamscaleServerOptions().isConfiguredForServerConnection()) { + LogToTeamscaleAppender.addTeamscaleAppenderTo(getLoggerContext(), agentOptions); + } } /** Closes the opened logging contexts. */ diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java index c8585712..48ee7865 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java @@ -131,5 +131,7 @@ private void unregisterProfiler() { } } - + public String getProfilerId() { + return profilerId; + } } diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java new file mode 100644 index 00000000..d5df72e5 --- /dev/null +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java @@ -0,0 +1,118 @@ +package com.teamscale.jacoco.agent.logging; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import com.teamscale.client.ProfilerLogEntry; +import com.teamscale.client.TeamscaleClient; +import com.teamscale.jacoco.agent.options.AgentOptions; +import retrofit2.Call; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +public class LogToTeamscaleAppender extends AppenderBase { + + private String profilerId; + private TeamscaleClient teamscaleClient; + private int batchSize = 10; + private Duration flushInterval = Duration.ofSeconds(3); + private final List logBuffer = new ArrayList<>(); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + public void setTeamscaleClient(TeamscaleClient teamscaleClient) { + this.teamscaleClient = teamscaleClient; + } + + public void setProfilerId(String profilerId) { + this.profilerId = profilerId; + } + + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } + + public void setFlushInterval(Duration flushInterval) { + this.flushInterval = flushInterval; + } + + @Override + public void start() { + super.start(); + scheduler.scheduleAtFixedRate(this::flush, flushInterval.toMillis(), flushInterval.toMillis(), TimeUnit.MILLISECONDS); + } + + @Override + protected void append(ILoggingEvent eventObject) { + synchronized (logBuffer) { + logBuffer.add(formatLog(eventObject)); + if (logBuffer.size() >= batchSize) { + flush(); + } + } + } + + private ProfilerLogEntry formatLog(ILoggingEvent eventObject) { + long timestamp = eventObject.getTimeStamp(); + String message = eventObject.getFormattedMessage(); + String severity = eventObject.getLevel().toString(); + return new ProfilerLogEntry(timestamp, message, severity); + } + + private void flush() { + List logsToSend; + synchronized (logBuffer) { + if (logBuffer.isEmpty()) { + return; + } + logsToSend = new ArrayList<>(logBuffer); + logBuffer.clear(); + } + sendLogs(logsToSend); + } + + private void sendLogs(List logs) { + CompletableFuture.runAsync(() -> { + try { + Call call = teamscaleClient.service.postProfilerLog(profilerId, logs); + retrofit2.Response response = call.execute(); + if (!response.isSuccessful()) { + throw new RuntimeException("Failed to send log: HTTP error code : " + response.code()); + } + } catch (Exception e) { + e.printStackTrace(); // Handle exceptions appropriately in production code + } + }); + } + + @Override + public void stop() { + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + } + flush(); // Ensure remaining logs are sent + super.stop(); + } + + + public static void addTeamscaleAppenderTo(LoggerContext context, AgentOptions agentOptions) { + LogToTeamscaleAppender logToTeamscaleAppender = new LogToTeamscaleAppender(); + logToTeamscaleAppender.setContext(context); + logToTeamscaleAppender.setProfilerId(agentOptions.configurationViaTeamscale.getProfilerId()); + logToTeamscaleAppender.setTeamscaleClient(agentOptions.createTeamscaleClient()); + logToTeamscaleAppender.start(); + + Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.addAppender(logToTeamscaleAppender); + } + + +} diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java index 46a53974..5f90300e 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java @@ -52,7 +52,10 @@ public static LoggingResources initializeDefaultLogging() { return new LoggingResources(); } - private static LoggerContext getLoggerContext() { + /** + * Returns the logger context. + */ + public static LoggerContext getLoggerContext() { return (LoggerContext) LoggerFactory.getILoggerFactory(); } diff --git a/common-system-test/src/main/java/com/teamscale/test/commons/TeamscaleMockServer.java b/common-system-test/src/main/java/com/teamscale/test/commons/TeamscaleMockServer.java index 2364596b..0d78f4c7 100644 --- a/common-system-test/src/main/java/com/teamscale/test/commons/TeamscaleMockServer.java +++ b/common-system-test/src/main/java/com/teamscale/test/commons/TeamscaleMockServer.java @@ -94,9 +94,10 @@ public TeamscaleMockServer withImpactedTests(String... impactedTests) { /** Configures the server to answer all impacted test calls with the given tests. */ public TeamscaleMockServer withProfilerConfiguration(ProfilerConfiguration profilerConfiguration) { this.profilerConfiguration = profilerConfiguration; - service.post("api/v2024.7.0/running-profilers", this::handleProfilerRegistration); - service.put("api/v2024.7.0/running-profilers/:profilerId", this::handleProfilerHeartbeat); - service.delete("api/v2024.7.0/running-profilers/:profilerId", this::handleProfilerUnregister); + service.post("api/v2024.7.0/profilers", this::handleProfilerRegistration); + service.put("api/v2024.7.0/profilers/:profilerId", this::handleProfilerHeartbeat); + service.delete("api/v2024.7.0/profilers/:profilerId", this::handleProfilerUnregister); + service.post("api/v2024.7.0/profilers/:profilerId/logs", this::handleProfilerLogs); return this; } @@ -146,6 +147,12 @@ private String handleProfilerHeartbeat(Request request, Response response) { return ""; } + private String handleProfilerLogs(Request request, Response response) { + collectedUserAgents.add(request.headers("User-Agent")); + profilerEvents.add("Profiler " + request.params(":profilerId") + " sent logs"); + return ""; + } + private String handleProfilerUnregister(Request request, Response response) { collectedUserAgents.add(request.headers("User-Agent")); profilerEvents.add("Profiler " + request.params(":profilerId") + " unregistered"); diff --git a/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java b/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java index 402ad9a8..50f3ab86 100644 --- a/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java +++ b/teamscale-client/src/main/java/com/teamscale/client/ITeamscaleService.java @@ -163,6 +163,12 @@ Call sendHeartbeat( @DELETE("api/v2024.7.0/profilers/{profilerId}") Call unregisterProfiler(@Path("profilerId") String profilerId); + @POST("api/v2024.7.0/profilers/{profilerId}/logs") + Call postProfilerLog( + @Path("profilerId") String profilerId, + @Body List logEntries + ); + /** * Uploads the given report body to Teamscale as blocking call with movetolastcommit set to false. * diff --git a/teamscale-client/src/main/java/com/teamscale/client/ProfilerLogEntry.java b/teamscale-client/src/main/java/com/teamscale/client/ProfilerLogEntry.java new file mode 100644 index 00000000..7ef2e566 --- /dev/null +++ b/teamscale-client/src/main/java/com/teamscale/client/ProfilerLogEntry.java @@ -0,0 +1,17 @@ +package com.teamscale.client; + +public class ProfilerLogEntry { + + private final long timestamp; + + private final String message; + + private final String severity; + + public ProfilerLogEntry(long timestamp, String message, String severity) { + this.timestamp = timestamp; + this.message = message; + this.severity = severity; + } + +}