diff --git a/README.md b/README.md
index bc78413e..e2d41773 100644
--- a/README.md
+++ b/README.md
@@ -157,7 +157,11 @@ The environment variables with the `DATADOG_JENKINS_PLUGIN` namespace take prece
#### Logging
-Logging is done by utilizing the `java.util.Logger`, which follows the [best logging practices for Jenkins][6]. To obtain logs, follow the directions in the [Jenkins logging documentation][6]. When adding a logger, all Datadog plugin functions start with `org.datadog.jenkins.plugins.datadog.` and the function name you are after should autopopulate. As of this writing, the only function available was `org.datadog.jenkins.plugins.datadog.listeners.DatadogBuildListener`.
+Logging is done by utilizing the `java.util.Logger`, which follows the [best logging practices for Jenkins][6].
+
+The plugin automatically registers a custom logger named "Datadog Plugin Logs" that writes the plugin's logs with level `INFO` or higher.
+The custom logger registration can be disabled by setting the `DD_JENKINS_PLUGIN_LOG_RECORDER_ENABLED` environment variable to `false`.
+If you with to see the plugin logs with maximum detail, manually change the level of the custom logger to `ALL`.
## Customization
@@ -422,6 +426,23 @@ NOTE: As mentioned in the [job customization](#job-customization) section, there
Build status `jenkins.job.status` with the default tags: : `jenkins_url`, `job`, `node`, `user_id`
+## Troubleshooting
+
+### Generating a diagnostic flare.
+
+Plugin diagnostic flare contains data that can be used to diagnose problems with the plugin.
+At the time of this writing the flare includes the following:
+- plugin configuration in XML format
+- plugin connectivity checks results
+- runtime data (current versions of JVM, Jenkins Core, plugin)
+- recent exceptions that happened inside the plugin code
+- plugin logs with level `INFO` and above, and recent Jenkins controller logs
+- current stacks of the threads of the Jenkins controller process
+- environment variables starting with `DD_` or `DATADOG_` (except API key and/or APP key)
+
+To generate a flare go to the `Manage Jenkins` page, find the `Troubleshooting` section, and select `Datadog`.
+Click on `Download Diagnostic Flare` (requires "MANAGE" permissions) to generate the flare.
+
## Issue tracking
GitHub's built-in issue tracking system is used to track all issues relating to this plugin: [jenkinsci/datadog-plugin/issues][7].
diff --git a/pom.xml b/pom.xml
index 0ba0059b..42de3612 100644
--- a/pom.xml
+++ b/pom.xml
@@ -205,6 +205,11 @@
bcpg-jdk18on
1.72
+
+ io.jenkins.lib
+ support-log-formatter
+ 1.2
+
diff --git a/src/main/.DS_Store b/src/main/.DS_Store
new file mode 100644
index 00000000..b6e62e1f
Binary files /dev/null and b/src/main/.DS_Store differ
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogPluginManagement.java b/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogPluginManagement.java
new file mode 100644
index 00000000..859d37ba
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogPluginManagement.java
@@ -0,0 +1,109 @@
+package org.datadog.jenkins.plugins.datadog;
+
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.ExtensionList;
+import hudson.model.ManagementLink;
+import hudson.security.Permission;
+import jenkins.model.Jenkins;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.datadog.jenkins.plugins.datadog.flare.FlareContributor;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.interceptor.RequirePOST;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+@Extension
+public class DatadogPluginManagement extends ManagementLink {
+
+ private static final Logger LOGGER = Logger.getLogger(DatadogPluginManagement.class.getName());
+
+ @CheckForNull
+ @Override
+ public String getIconFileName() {
+ return "/plugin/datadog/icons/dd_icon_rgb.svg";
+ }
+
+ @CheckForNull
+ @Override
+ public String getDisplayName() {
+ return "Datadog";
+ }
+
+ @CheckForNull
+ @Override
+ public String getUrlName() {
+ return "datadog";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Datadog Plugin Troubleshooting";
+ }
+
+ @NonNull
+ @Override
+ public Category getCategory() {
+ return Category.TROUBLESHOOTING;
+ }
+
+ @NonNull
+ @Override
+ public Permission getRequiredPermission() {
+ return Jenkins.MANAGE;
+ }
+
+ @RequirePOST
+ public void doDownloadDiagnosticFlare(StaplerRequest request, StaplerResponse response) throws Exception {
+ if (!Jenkins.get().hasPermission(Jenkins.MANAGE)) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ try {
+ LocalDateTime now = LocalDateTime.now();
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss");
+ String formattedTimestamp = now.format(formatter);
+
+ response.setContentType("application/octet-stream");
+ response.setHeader("Content-Disposition", String.format("attachment; filename=dd-jenkins-plugin-flare-%s.zip", formattedTimestamp));
+ try (OutputStream out = response.getOutputStream()) {
+ writeDiagnosticFlare(out);
+ }
+
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "Failed to generate Datadog plugin flare", e);
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ private void writeDiagnosticFlare(OutputStream out) throws IOException {
+ ExtensionList contributors = ExtensionList.lookup(FlareContributor.class);
+ try (ZipOutputStream zipOut = new ZipOutputStream(out)) {
+ for (FlareContributor contributor : contributors) {
+ zipOut.putNextEntry(new ZipEntry(contributor.getFilename()));
+ try {
+ contributor.writeFileContents(zipOut);
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "Datadog plugin flare contributor failed: " + contributor.getClass(), e);
+
+ zipOut.closeEntry();
+ zipOut.putNextEntry(new ZipEntry(contributor.getFilename() + ".error"));
+ zipOut.write(ExceptionUtils.getStackTrace(e).getBytes());
+ } finally {
+ zipOut.closeEntry();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogUtilities.java b/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogUtilities.java
index 42688c0b..17c99b39 100644
--- a/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogUtilities.java
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogUtilities.java
@@ -34,6 +34,7 @@ of this software and associated documentation files (the "Software"), to deal
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.commons.lang3.tuple.Pair;
import org.datadog.jenkins.plugins.datadog.apm.ShellCommandCallable;
import org.datadog.jenkins.plugins.datadog.clients.HttpClient;
import org.datadog.jenkins.plugins.datadog.model.DatadogPluginAction;
@@ -59,6 +60,8 @@ of this software and associated documentation files (the "Software"), to deal
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -954,6 +957,8 @@ public static void severe(Logger logger, Throwable e, String message) {
public static void logException(Logger logger, Level logLevel, String message, Throwable e) {
if (e != null) {
+ addExceptionToBuffer(e);
+
String stackTrace = ExceptionUtils.getStackTrace(e);
message = (message != null ? message + " " : "An unexpected error occurred: ") + stackTrace;
}
@@ -962,6 +967,49 @@ public static void logException(Logger logger, Level logLevel, String message, T
}
}
+ private static final String EXCEPTIONS_BUFFER_CAPACITY_ENV_VAR = "DD_JENKINS_EXCEPTIONS_BUFFER_CAPACITY";
+ private static final int DEFAULT_EXCEPTIONS_BUFFER_CAPACITY = 100;
+ private static final BlockingQueue> EXCEPTIONS_BUFFER;
+
+ static {
+ int bufferCapacity = getExceptionsBufferCapacity();
+ if (bufferCapacity > 0) {
+ EXCEPTIONS_BUFFER = new ArrayBlockingQueue<>(bufferCapacity);
+ } else {
+ EXCEPTIONS_BUFFER = null;
+ }
+ }
+
+ private static int getExceptionsBufferCapacity() {
+ String bufferCapacityString = System.getenv("EXCEPTIONS_BUFFER_CAPACITY_ENV_VAR");
+ if (bufferCapacityString == null) {
+ return DEFAULT_EXCEPTIONS_BUFFER_CAPACITY;
+ } else {
+ try {
+ return Integer.parseInt(bufferCapacityString);
+ } catch (NumberFormatException e) {
+ severe(logger, e, EXCEPTIONS_BUFFER_CAPACITY_ENV_VAR + " environment variable has invalid value");
+ return 0;
+ }
+ }
+ }
+
+ private static void addExceptionToBuffer(Throwable e) {
+ if (EXCEPTIONS_BUFFER == null) {
+ return;
+ }
+ Pair p = Pair.of(new Date(), e);
+ while (!EXCEPTIONS_BUFFER.offer(p)) {
+ // rather than popping elements one by one, we drain several with one operation to reduce lock contention
+ int drainSize = Math.max(DEFAULT_EXCEPTIONS_BUFFER_CAPACITY / 10, 1);
+ EXCEPTIONS_BUFFER.drainTo(new ArrayList<>(drainSize), drainSize);
+ }
+ }
+
+ public static BlockingQueue> getExceptionsBuffer() {
+ return EXCEPTIONS_BUFFER;
+ }
+
public static int toInt(boolean b) {
return b ? 1 : 0;
}
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogApiClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogApiClient.java
index b1afc4e7..0efb2ec3 100644
--- a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogApiClient.java
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogApiClient.java
@@ -138,7 +138,7 @@ public static boolean validateDefaultIntakeConnection(String validatedUrl, Secre
}
@SuppressFBWarnings("DLS_DEAD_LOCAL_STORE")
- private static boolean validateWebhookIntakeConnection(String webhookIntakeUrl, Secret apiKey) {
+ public static boolean validateWebhookIntakeConnection(String webhookIntakeUrl, Secret apiKey) {
Map headers = new HashMap<>();
headers.put("DD-API-KEY", Secret.toString(apiKey));
@@ -157,7 +157,7 @@ private static boolean validateWebhookIntakeConnection(String webhookIntakeUrl,
}
}
- private static boolean validateLogIntakeConnection(String logsIntakeUrl, Secret apiKey) {
+ public static boolean validateLogIntakeConnection(String logsIntakeUrl, Secret apiKey) {
String payload = "{\"message\":\"[datadog-plugin] Check connection\", " +
"\"ddsource\":\"Jenkins\", \"service\":\"Jenkins\", " +
"\"hostname\":\"" + DatadogUtilities.getHostname(null) + "\"}";
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/flare/ConnectivityChecksFlare.java b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/ConnectivityChecksFlare.java
new file mode 100644
index 00000000..5d377307
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/ConnectivityChecksFlare.java
@@ -0,0 +1,49 @@
+package org.datadog.jenkins.plugins.datadog.flare;
+
+import hudson.Extension;
+import net.sf.json.JSONObject;
+import org.apache.commons.io.IOUtils;
+import org.datadog.jenkins.plugins.datadog.DatadogClient;
+import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration;
+import org.datadog.jenkins.plugins.datadog.DatadogUtilities;
+import org.datadog.jenkins.plugins.datadog.clients.DatadogApiClient;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+@Extension
+public class ConnectivityChecksFlare implements FlareContributor {
+
+ @Override
+ public String getFilename() {
+ return "connectivity-checks.json";
+ }
+
+ @Override
+ public void writeFileContents(OutputStream out) throws IOException {
+ JSONObject payload = new JSONObject();
+
+ // TODO rework the checks below following configuration refactoring
+ DatadogGlobalConfiguration globalConfiguration = DatadogUtilities.getDatadogGlobalDescriptor();
+ DatadogClient.ClientType clientType = DatadogClient.ClientType.valueOf(globalConfiguration.getReportWith());
+
+ if (clientType == DatadogClient.ClientType.DSD) {
+ payload.put("client-type", DatadogClient.ClientType.DSD);
+ payload.put("logs-connectivity", globalConfiguration.doCheckAgentConnectivityLogs(globalConfiguration.getTargetHost(), String.valueOf(globalConfiguration.getTargetLogCollectionPort())).toString());
+ payload.put("traces-connectivity", globalConfiguration.doCheckAgentConnectivityTraces(globalConfiguration.getTargetHost(), String.valueOf(globalConfiguration.getTargetTraceCollectionPort())).toString());
+
+ } else if (clientType == DatadogClient.ClientType.HTTP) {
+ payload.put("client-type", DatadogClient.ClientType.HTTP);
+ payload.put("api-connectivity", DatadogApiClient.validateDefaultIntakeConnection(globalConfiguration.getTargetApiURL(), globalConfiguration.getUsedApiKey()));
+ payload.put("logs-connectivity", DatadogApiClient.validateLogIntakeConnection(globalConfiguration.getTargetLogIntakeURL(), globalConfiguration.getUsedApiKey()));
+ payload.put("traces-connectivity", DatadogApiClient.validateWebhookIntakeConnection(globalConfiguration.getTargetWebhookIntakeURL(), globalConfiguration.getUsedApiKey()));
+
+ } else {
+ throw new IllegalArgumentException("Unsupported client type: " + clientType);
+ }
+
+ String payloadString = payload.toString(2);
+ IOUtils.write(payloadString, out, StandardCharsets.UTF_8);
+ }
+}
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/flare/DatadogConfigFlare.java b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/DatadogConfigFlare.java
new file mode 100644
index 00000000..383ce24e
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/DatadogConfigFlare.java
@@ -0,0 +1,32 @@
+package org.datadog.jenkins.plugins.datadog.flare;
+
+import hudson.Extension;
+import hudson.util.XStream2;
+import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration;
+import org.datadog.jenkins.plugins.datadog.DatadogUtilities;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+@Extension
+public class DatadogConfigFlare implements FlareContributor {
+
+ // TODO use XSTREAM from DatadogGlobalConfiguration following configuration refactor
+ private static final XStream2 XSTREAM;
+
+ static {
+ XSTREAM = new XStream2();
+ XSTREAM.autodetectAnnotations(true);
+ }
+
+ @Override
+ public String getFilename() {
+ return "DatadogGlobalConfiguration.xml";
+ }
+
+ @Override
+ public void writeFileContents(OutputStream out) throws IOException {
+ DatadogGlobalConfiguration globalConfiguration = DatadogUtilities.getDatadogGlobalDescriptor();
+ XSTREAM.toXMLUTF8(globalConfiguration, out);
+ }
+}
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/flare/DatadogEnvVarsFlare.java b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/DatadogEnvVarsFlare.java
new file mode 100644
index 00000000..23494920
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/DatadogEnvVarsFlare.java
@@ -0,0 +1,32 @@
+package org.datadog.jenkins.plugins.datadog.flare;
+
+import hudson.Extension;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.Properties;
+
+@Extension
+public class DatadogEnvVarsFlare implements FlareContributor {
+
+ @Override
+ public String getFilename() {
+ return "dd-env-vars.properties";
+ }
+
+ @Override
+ public void writeFileContents(OutputStream out) throws IOException {
+ Properties datadogVariables = new Properties();
+ for (Map.Entry e : System.getenv().entrySet()) {
+ String name = e.getKey();
+ if (name.startsWith("DD_") || name.startsWith("DATADOG_")) {
+ if (!name.contains("API_KEY") && !name.contains("APP_KEY")) {
+ datadogVariables.put(name, e.getValue());
+ }
+ }
+ }
+ datadogVariables.store(out, "Environment variables prefixed with DD_ or DATADOG_");
+ }
+
+}
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/flare/ExceptionsFlare.java b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/ExceptionsFlare.java
new file mode 100644
index 00000000..26ed031e
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/ExceptionsFlare.java
@@ -0,0 +1,39 @@
+package org.datadog.jenkins.plugins.datadog.flare;
+
+import hudson.Extension;
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.datadog.jenkins.plugins.datadog.DatadogUtilities;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.BlockingQueue;
+
+@Extension
+public class ExceptionsFlare implements FlareContributor {
+
+ public static final DateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS Z");
+
+ @Override
+ public String getFilename() {
+ return "exceptions.txt";
+ }
+
+ @Override
+ public void writeFileContents(OutputStream out) {
+ // Print writer is not closed intentionally, to avoid closing out.
+ // Auto-flush set to true ensures everything is witten
+ PrintWriter printWriter = new PrintWriter(out, true);
+
+ BlockingQueue> exceptionsBuffer = DatadogUtilities.getExceptionsBuffer();
+ for (Pair p : exceptionsBuffer) {
+ Date date = p.getKey();
+ Throwable exception = p.getValue();
+ printWriter.println(DATE_FORMATTER.format(date) + ": " + ExceptionUtils.getStackTrace(exception));
+ }
+ }
+}
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/flare/FlareContributor.java b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/FlareContributor.java
new file mode 100644
index 00000000..2a084620
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/FlareContributor.java
@@ -0,0 +1,13 @@
+package org.datadog.jenkins.plugins.datadog.flare;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public interface FlareContributor {
+
+ String getFilename();
+
+ void writeFileContents(OutputStream out) throws IOException;
+
+}
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/flare/JenkinsLogsFlare.java b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/JenkinsLogsFlare.java
new file mode 100644
index 00000000..0362ff78
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/JenkinsLogsFlare.java
@@ -0,0 +1,44 @@
+package org.datadog.jenkins.plugins.datadog.flare;
+
+import hudson.Extension;
+import io.jenkins.lib.support_log_formatter.SupportLogFormatter;
+import jenkins.model.Jenkins;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.logging.Formatter;
+import java.util.logging.LogRecord;
+
+/**
+ * This flare writes only the last small part of the Jenkins controller logs
+ * that is still available in the in-memory ring-buffer.
+ * The full logs that are stored on disk are not written intentionally.
+ * They're available in the support bundles generated with Cloudbees or Support Core plugins.
+ */
+@Extension
+public class JenkinsLogsFlare implements FlareContributor {
+
+ private static final Formatter LOG_FORMATTER = new SupportLogFormatter();
+
+ @Override
+ public String getFilename() {
+ return "jenkins.log";
+ }
+
+ @Override
+ public void writeFileContents(OutputStream out) {
+ // Print writer is not closed intentionally, to avoid closing out.
+ // Auto-flush set to true ensures everything is witten
+ PrintWriter printWriter = new PrintWriter(out, true);
+
+ List logRecords = Jenkins.logRecords;
+ ListIterator it = logRecords.listIterator(logRecords.size());
+ while (it.hasPrevious()) {
+ LogRecord logRecord = it.previous();
+ String formatted = LOG_FORMATTER.format(logRecord);
+ printWriter.print(formatted);
+ }
+ }
+}
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/flare/PluginLogsFlare.java b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/PluginLogsFlare.java
new file mode 100644
index 00000000..d858fa70
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/PluginLogsFlare.java
@@ -0,0 +1,84 @@
+package org.datadog.jenkins.plugins.datadog.flare;
+
+import hudson.Extension;
+import hudson.init.InitMilestone;
+import hudson.init.Initializer;
+import hudson.logging.LogRecorder;
+import hudson.logging.LogRecorderManager;
+import io.jenkins.lib.support_log_formatter.SupportLogFormatter;
+import jenkins.model.Jenkins;
+import org.datadog.jenkins.plugins.datadog.DatadogUtilities;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.logging.Formatter;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+@Extension
+public class PluginLogsFlare implements FlareContributor {
+
+ private static final Logger LOGGER = Logger.getLogger(PluginLogsFlare.class.getName());
+
+ private static final Formatter LOG_FORMATTER = new SupportLogFormatter();
+
+ private static final String DATADOG_PLUGIN_LOGS = "Datadog Plugin Logs";
+
+ private static final String PLUGIN_LOG_RECORDER_ENABLED_ENV_VAR = "DD_JENKINS_PLUGIN_LOG_RECORDER_ENABLED";
+
+ @Initializer(after = InitMilestone.SYSTEM_CONFIG_LOADED)
+ public void onStartup() {
+ String pluginLogRecorderEnabled = System.getenv(PLUGIN_LOG_RECORDER_ENABLED_ENV_VAR);
+ if (pluginLogRecorderEnabled != null && !Boolean.parseBoolean(pluginLogRecorderEnabled)) {
+ return;
+ }
+
+ try {
+ LogRecorderManager logRecorderManager = Jenkins.get().getLog();
+ LogRecorder logRecorder = logRecorderManager.getLogRecorder(DATADOG_PLUGIN_LOGS);
+ if (logRecorder == null) {
+ logRecorderManager.doNewLogRecorder(DATADOG_PLUGIN_LOGS);
+ logRecorder = logRecorderManager.getLogRecorder(DATADOG_PLUGIN_LOGS);
+ }
+
+ LogRecorder.Target target = new LogRecorder.Target("org.datadog", Level.INFO);
+ logRecorder.setLoggers(Collections.singletonList(target));
+ logRecorder.save();
+ } catch (Exception e) {
+ DatadogUtilities.severe(LOGGER, e, "Could not register Datadog plugin log recorder");
+ }
+ }
+
+ @Override
+ public String getFilename() {
+ return "plugin.log";
+ }
+
+ @Override
+ public void writeFileContents(OutputStream out) {
+ // Print writer is not closed intentionally, to avoid closing out.
+ // Auto-flush set to true ensures everything is witten
+ PrintWriter printWriter = new PrintWriter(out, true);
+
+ Jenkins jenkins = Jenkins.get();
+ LogRecorderManager logRecorderManager = jenkins.getLog();
+ LogRecorder logRecorder = logRecorderManager.getLogRecorder(DATADOG_PLUGIN_LOGS);
+ if (logRecorder == null) {
+ printWriter.println("Log recorder " + DATADOG_PLUGIN_LOGS + " not found.");
+ return;
+ }
+
+ List logRecords = logRecorder.getLogRecords();
+
+ ListIterator it = logRecords.listIterator(logRecords.size());
+ while (it.hasPrevious()) {
+ LogRecord logRecord = it.previous();
+ String formatted = LOG_FORMATTER.format(logRecord);
+ printWriter.print(formatted);
+ }
+ }
+}
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/flare/RuntimeInfoFlare.java b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/RuntimeInfoFlare.java
new file mode 100644
index 00000000..b429ff16
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/RuntimeInfoFlare.java
@@ -0,0 +1,36 @@
+package org.datadog.jenkins.plugins.datadog.flare;
+
+import hudson.Extension;
+import jenkins.model.Jenkins;
+import net.sf.json.JSONObject;
+import org.apache.commons.io.IOUtils;
+import org.datadog.jenkins.plugins.datadog.DatadogUtilities;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+@Extension
+public class RuntimeInfoFlare implements FlareContributor {
+
+ @Override
+ public String getFilename() {
+ return "runtime-info.json";
+ }
+
+ @Override
+ public void writeFileContents(OutputStream out) throws IOException {
+ JSONObject payload = new JSONObject();
+ payload.put("java-runtime-name", System.getProperty("java.runtime.name"));
+ payload.put("java-version", System.getProperty("java.version"));
+ payload.put("java-vendor", System.getProperty("java.vendor"));
+ payload.put("os-architecture", System.getProperty("os.arch"));
+ payload.put("os-name", System.getProperty("os.name"));
+ payload.put("os-version", System.getProperty("os.version"));
+ payload.put("jenkins-version", Jenkins.getVersion().toString());
+ payload.put("plugin-version", DatadogUtilities.getDatadogPluginVersion());
+
+ String payloadString = payload.toString(2);
+ IOUtils.write(payloadString, out, StandardCharsets.UTF_8);
+ }
+}
diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/flare/ThreadDumpFlare.java b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/ThreadDumpFlare.java
new file mode 100644
index 00000000..3a7e7b60
--- /dev/null
+++ b/src/main/java/org/datadog/jenkins/plugins/datadog/flare/ThreadDumpFlare.java
@@ -0,0 +1,36 @@
+package org.datadog.jenkins.plugins.datadog.flare;
+
+import hudson.Extension;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Map;
+
+@Extension
+public class ThreadDumpFlare implements FlareContributor {
+
+ @Override
+ public String getFilename() {
+ return "thread-dump.txt";
+ }
+
+ @Override
+ public void writeFileContents(OutputStream out) {
+ // Print writer is not closed intentionally, to avoid closing out.
+ // Auto-flush set to true ensures everything is witten
+ PrintWriter printWriter = new PrintWriter(out, true);
+
+ Map allStackTraces = Thread.getAllStackTraces();
+ for (Map.Entry entry : allStackTraces.entrySet()) {
+ Thread thread = entry.getKey();
+ StackTraceElement[] stackTrace = entry.getValue();
+
+ printWriter.println("Thread: " + thread.getName());
+ for (StackTraceElement element : stackTrace) {
+ printWriter.println(" at " + element);
+ }
+ printWriter.println();
+ }
+ }
+}
diff --git a/src/main/resources/org/datadog/jenkins/plugins/datadog/DatadogPluginManagement/index.jelly b/src/main/resources/org/datadog/jenkins/plugins/datadog/DatadogPluginManagement/index.jelly
new file mode 100644
index 00000000..1e39c2dd
--- /dev/null
+++ b/src/main/resources/org/datadog/jenkins/plugins/datadog/DatadogPluginManagement/index.jelly
@@ -0,0 +1,17 @@
+
+
+
+
+ Datadog Plugin Management
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/icons/dd_icon_rgb.svg b/src/main/webapp/icons/dd_icon_rgb.svg
new file mode 100644
index 00000000..f39a6ac1
--- /dev/null
+++ b/src/main/webapp/icons/dd_icon_rgb.svg
@@ -0,0 +1,41 @@
+
+
+