From 7edec111cf3757efaa1cf1329d2574c16785e7a1 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko Date: Fri, 19 Jan 2024 15:48:32 +0100 Subject: [PATCH 1/9] Rework CI Visibility spans batching --- .../plugins/datadog/DatadogClient.java | 83 +---- .../datadog/DatadogGlobalConfiguration.java | 11 +- .../plugins/datadog/DatadogUtilities.java | 13 - .../datadog/clients/ClientFactory.java | 4 +- .../datadog/clients/DatadogAgentClient.java | 322 ++++-------------- ...gHttpClient.java => DatadogApiClient.java} | 229 ++++--------- .../plugins/datadog/clients/HttpClient.java | 25 +- .../listeners/DatadogBuildListener.java | 38 ++- .../listeners/DatadogGraphListener.java | 17 +- .../datadog/traces/DatadogBaseBuildLogic.java | 11 +- .../traces/DatadogBasePipelineLogic.java | 11 +- .../traces/DatadogTraceBuildLogic.java | 57 +--- .../traces/DatadogTracePipelineLogic.java | 62 ++-- .../traces/DatadogWebhookBuildLogic.java | 48 +-- .../traces/DatadogWebhookPipelineLogic.java | 51 +-- .../traces/mapper/JsonTraceSpanMapper.java | 89 ++--- .../datadog/traces/message/TraceSpan.java | 11 +- .../traces/write/AgentTraceWriteStrategy.java | 96 ++++++ .../traces/write/TraceWriteStrategy.java | 16 + .../traces/write/TraceWriteStrategyImpl.java | 56 +++ .../datadog/traces/write/TraceWriter.java | 118 +++++++ .../traces/write/TraceWriterFactory.java | 30 ++ .../plugins/datadog/transport/HttpClient.java | 12 - .../datadog/transport/HttpErrorHandler.java | 6 - .../datadog/transport/HttpMessage.java | 50 --- .../datadog/transport/HttpMessageFactory.java | 55 --- .../plugins/datadog/transport/HttpSender.java | 77 ----- .../transport/LoggerHttpErrorHandler.java | 17 - .../transport/NonBlockingHttpClient.java | 140 -------- .../datadog/transport/PayloadMapper.java | 10 - .../datadog/transport/PayloadMessage.java | 10 - .../plugins/datadog/util/CircuitBreaker.java | 83 +++++ .../datadog/clients/DatadogClientStub.java | 224 +++++------- .../datadog/clients/DatadogClientTest.java | 32 +- .../listeners/DatadogBuildListenerIT.java | 72 ++-- .../listeners/DatadogGraphListenerTest.java | 121 +++---- .../mapper/JsonTraceSpanMapperTest.java | 45 +-- .../datadog/transport/FakeHttpSender.java | 70 ---- .../transport/FakeTracesHttpClient.java | 85 ----- .../datadog/transport/HttpSenderTest.java | 44 --- 40 files changed, 899 insertions(+), 1652 deletions(-) rename src/main/java/org/datadog/jenkins/plugins/datadog/clients/{DatadogHttpClient.java => DatadogApiClient.java} (75%) create mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java create mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java create mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java create mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java create mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java delete mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpClient.java delete mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpErrorHandler.java delete mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpMessage.java delete mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpMessageFactory.java delete mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpSender.java delete mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/transport/LoggerHttpErrorHandler.java delete mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/transport/NonBlockingHttpClient.java delete mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/transport/PayloadMapper.java delete mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/transport/PayloadMessage.java create mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/util/CircuitBreaker.java delete mode 100644 src/test/java/org/datadog/jenkins/plugins/datadog/transport/FakeHttpSender.java delete mode 100644 src/test/java/org/datadog/jenkins/plugins/datadog/transport/FakeTracesHttpClient.java delete mode 100644 src/test/java/org/datadog/jenkins/plugins/datadog/transport/HttpSenderTest.java diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogClient.java index b5508a66e..eb72d033a 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogClient.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogClient.java @@ -26,24 +26,21 @@ of this software and associated documentation files (the "Software"), to deal package org.datadog.jenkins.plugins.datadog; import com.timgroup.statsd.ServiceCheck; -import hudson.model.Run; -import hudson.util.Secret; import java.util.Map; import java.util.Set; import org.datadog.jenkins.plugins.datadog.clients.Metrics; -import org.datadog.jenkins.plugins.datadog.model.BuildData; -import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; public interface DatadogClient { - public static enum ClientType { + enum ClientType { HTTP, DSD; - private ClientType() { } + ClientType() { } } - public static enum Status { + enum Status { OK(0), WARNING(1), CRITICAL(2), @@ -51,7 +48,7 @@ public static enum Status { private final int val; - private Status(int val) { + Status(int val) { this.val = val; } @@ -64,39 +61,13 @@ public ServiceCheck.Status toServiceCheckStatus(){ } } - public void setUrl(String url); - - public void setLogIntakeUrl(String logIntakeUrl); - - public void setWebhookIntakeUrl(String webhookIntakeUrl); - - public void setApiKey(Secret apiKey); - - public void setHostname(String hostname); - - public void setPort(Integer port); - - public void setLogCollectionPort(Integer logCollectionPort); - - public boolean isDefaultIntakeConnectionBroken(); - - public void setDefaultIntakeConnectionBroken(boolean defaultIntakeConnectionBroken); - - public boolean isLogIntakeConnectionBroken(); - - public boolean isWebhookIntakeConnectionBroken(); - - public void setLogIntakeConnectionBroken(boolean logIntakeConnectionBroken); - - public void setWebhookIntakeConnectionBroken(boolean webhookIntakeConnectionBroken); - /** * Sends an event to the Datadog API, including the event payload. * * @param event - a DatadogEvent object * @return a boolean to signify the success or failure of the HTTP POST request. */ - public boolean event(DatadogEvent event); + boolean event(DatadogEvent event); /** * Increment a counter for the given metrics. @@ -107,14 +78,14 @@ public ServiceCheck.Status toServiceCheckStatus(){ * @param tags - metric tags * @return a boolean to signify the success or failure of increment submission. */ - public boolean incrementCounter(String name, String hostname, Map> tags); + boolean incrementCounter(String name, String hostname, Map> tags); /** * Submit all your counters as rate with 10 seconds intervals. */ - public void flushCounters(); + void flushCounters(); - public Metrics metrics(); + Metrics metrics(); /** * Sends a service check to the Datadog API, including the check name, and status. @@ -125,45 +96,15 @@ public ServiceCheck.Status toServiceCheckStatus(){ * @param tags - A Map containing the tags to submit. * @return a boolean to signify the success or failure of the HTTP POST request. */ - public boolean serviceCheck(String name, Status status, String hostname, Map> tags); + boolean serviceCheck(String name, Status status, String hostname, Map> tags); /** * Send log message. * @param payload log payload to submit JSON object as String * @return a boolean to signify the success or failure of the request. */ - public boolean sendLogs(String payload); + boolean sendLogs(String payload); - /** - * Send a webhook payload to the webhooks intake. - * - * @param payload - A webhooks payload. - * @return a boolean to signify the success or failure of the HTTP POST request. - */ - public boolean postWebhook(String payload); - - /** - * Start the trace of a certain Jenkins build. - * @param buildData build data to use in the pipeline trace - * @param run a particular execution of a Jenkins build - * @return a boolean to signify the success or failure of the request. - */ - boolean startBuildTrace(BuildData buildData, Run run); - - /** - * Finish the trace of a certain Jenkins build. - * @param buildData build data to use in the pipeline trace - * @param run the run to create a pipeline trace for - * @return a boolean to signify the success or failure of the request. - */ - boolean finishBuildTrace(BuildData buildData, Run run); - - /** - * Send all traces related to a certain Jenkins pipeline. - * @param run a particular execution of a Jenkins build - * @param flowNode current flowNode - * @return a boolean to signify the success or failure of the request. - */ - boolean sendPipelineTrace(Run run, FlowNode flowNode); + TraceWriteStrategy createTraceWriteStrategy(); } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogGlobalConfiguration.java b/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogGlobalConfiguration.java index 7675ccc5f..54c3b3e60 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogGlobalConfiguration.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogGlobalConfiguration.java @@ -44,13 +44,14 @@ of this software and associated documentation files (the "Software"), to deal import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import org.datadog.jenkins.plugins.datadog.clients.HttpClient; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriterFactory; import org.jenkinsci.plugins.plaincredentials.StringCredentials; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.datadog.jenkins.plugins.datadog.clients.ClientFactory; -import org.datadog.jenkins.plugins.datadog.clients.DatadogHttpClient; +import org.datadog.jenkins.plugins.datadog.clients.DatadogApiClient; import org.datadog.jenkins.plugins.datadog.clients.DatadogAgentClient; import org.datadog.jenkins.plugins.datadog.util.SuppressFBWarnings; import org.datadog.jenkins.plugins.datadog.util.config.DatadogAgentConfiguration; @@ -445,7 +446,7 @@ public FormValidation doTestConnection( throws IOException, ServletException { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); final Secret secret = findSecret(targetApiKey, targetCredentialsApiKey); - if (DatadogHttpClient.validateDefaultIntakeConnection(new HttpClient(60_000), targetApiURL, secret)) { + if (DatadogApiClient.validateDefaultIntakeConnection(new HttpClient(60_000), targetApiURL, secret)) { return FormValidation.ok("Great! Your API key is valid."); } else { return FormValidation.error("Hmmm, your API key seems to be invalid."); @@ -839,9 +840,9 @@ public boolean configure(final StaplerRequest req, final JSONObject formData) th if(client == null) { return false; } - client.setDefaultIntakeConnectionBroken(false); - client.setLogIntakeConnectionBroken(false); - client.setWebhookIntakeConnectionBroken(false); + + TraceWriterFactory.onDatadogClientUpdate(client); + // Persist global configuration information save(); return true; 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 d4035b93c..a2107fb06 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogUtilities.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/DatadogUtilities.java @@ -987,19 +987,6 @@ public static boolean isPipeline(final Run run) { return run != null && run.getAction(IsPipelineAction.class) != null; } - /** - * Returns an HTTP URL - * - * @param hostname - the Hostname - * @param port - the port to use - * @param path - the path - * @return the HTTP URL - * @throws MalformedURLException if the URL is not in a valid format - */ - public static URL buildHttpURL(final String hostname, final Integer port, final String path) throws MalformedURLException { - return new URL(String.format("http://%s:%d" + path, hostname, port)); - } - public static String getCatchErrorResult(BlockStartNode startNode) { String displayFunctionName = startNode.getDisplayFunctionName(); if ("warnError".equals(displayFunctionName)) { diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/ClientFactory.java b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/ClientFactory.java index b8c2a83bc..90ec8d161 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/ClientFactory.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/ClientFactory.java @@ -29,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal 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.traces.write.TraceWriterFactory; public class ClientFactory { private static DatadogClient testClient; @@ -36,6 +37,7 @@ public class ClientFactory { public static void setTestClient(DatadogClient testClient){ // Only used for tests ClientFactory.testClient = testClient; + TraceWriterFactory.onDatadogClientUpdate(testClient); } public static DatadogClient getClient(DatadogClient.ClientType type, String apiUrl, String logIntakeUrl, @@ -47,7 +49,7 @@ public static DatadogClient getClient(DatadogClient.ClientType type, String apiU } switch(type){ case HTTP: - return DatadogHttpClient.getInstance(apiUrl, logIntakeUrl, webhookIntakeUrl, apiKey); + return DatadogApiClient.getInstance(apiUrl, logIntakeUrl, webhookIntakeUrl, apiKey); case DSD: return DatadogAgentClient.getInstance(host, port, logCollectionPort, traceCollectionPort); default: diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java index c75816d3e..b73756625 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java @@ -25,26 +25,22 @@ of this software and associated documentation files (the "Software"), to deal package org.datadog.jenkins.plugins.datadog.clients; -import static org.datadog.jenkins.plugins.datadog.DatadogUtilities.buildHttpURL; -import static org.datadog.jenkins.plugins.datadog.transport.LoggerHttpErrorHandler.LOGGER_HTTP_ERROR_HANDLER; - import com.timgroup.statsd.Event; import com.timgroup.statsd.NonBlockingStatsDClient; import com.timgroup.statsd.ServiceCheck; import com.timgroup.statsd.StatsDClient; -import hudson.model.Run; -import hudson.util.Secret; import java.net.ConnectException; import java.net.InetAddress; import java.net.Socket; -import java.net.URL; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.logging.Handler; import java.util.logging.Logger; import java.util.logging.SocketHandler; @@ -53,21 +49,16 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.DatadogEvent; import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; -import org.datadog.jenkins.plugins.datadog.model.BuildData; -import org.datadog.jenkins.plugins.datadog.traces.DatadogBaseBuildLogic; -import org.datadog.jenkins.plugins.datadog.traces.DatadogBasePipelineLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogTraceBuildLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogTracePipelineLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookBuildLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookPipelineLogic; import org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper; -import org.datadog.jenkins.plugins.datadog.transport.HttpMessage; -import org.datadog.jenkins.plugins.datadog.transport.HttpMessageFactory; -import org.datadog.jenkins.plugins.datadog.transport.NonBlockingHttpClient; -import org.datadog.jenkins.plugins.datadog.transport.PayloadMessage; +import org.datadog.jenkins.plugins.datadog.traces.write.AgentTraceWriteStrategy; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategyImpl; import org.datadog.jenkins.plugins.datadog.util.SuppressFBWarnings; import org.datadog.jenkins.plugins.datadog.util.TagsUtil; -import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.json.JSONArray; import org.json.JSONObject; @@ -80,43 +71,26 @@ public class DatadogAgentClient implements DatadogClient { private static volatile DatadogAgentClient instance = null; // Used to determine if the instance failed last validation last time, so // we do not keep retrying to create the instance and logging the same error - private static boolean failedLastValidation = false; + private static volatile boolean failedLastValidation = false; private static final Logger logger = Logger.getLogger(DatadogAgentClient.class.getName()); - @SuppressFBWarnings(value="MS_SHOULD_BE_FINAL") public static boolean enableValidations = true; - private org.datadog.jenkins.plugins.datadog.transport.HttpClient agentHttpClient; - private DatadogBaseBuildLogic traceBuildLogic; - private DatadogBasePipelineLogic tracePipelineLogic; - private StatsDClient statsd; private Logger ddLogger; private String previousPayload; - private String hostname = null; + private final String hostname; + private final Integer port; + private final Integer logCollectionPort; + private final Integer traceCollectionPort; private String resolvedIp = ""; - private Integer port = null; - private Integer logCollectionPort = null; - private Integer traceCollectionPort = null; private boolean isStoppedStatsDClient = true; - private boolean isStoppedAgentHttpClient = true; - private boolean evpProxySupported = false; - private long lastEvpProxyCheckTimeMs = 0L; private final HttpClient client; - /** - * How often to check the /info endpoint in case the Agent got updated. - */ - private static final int EVP_PROXY_SUPPORT_TIME_BETWEEN_CHECKS_MS = 1*60*60*1000; - - /** - * Timeout waiting for a reply after a connection to the /info endpoint was established. - */ - private static final int HTTP_TIMEOUT_INFO_MS = 10 * 1000; /** * Timeout of 1 minutes for connecting and reading via the synchronous Agent EVP Proxy. @@ -163,7 +137,6 @@ public static DatadogClient getInstance(String hostname, Integer port, Integer l if (instance != null){ instance.reinitializeStatsDClient(true); instance.reinitializeLogger(true); - instance.reinitializeAgentHttpClient(true); } return instance; } @@ -199,7 +172,6 @@ public void validateConfiguration() throws IllegalArgumentException { if (DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility() && traceCollectionPort == null) { logger.warning("Datadog Trace Collection Port is not set properly"); } - return; } @Override @@ -365,120 +337,6 @@ public Set fetchAgentSupportedEndpoints() { } } - /** - * Posts a given payload to the Agent EVP Proxy so it is forwarded to the Webhook Intake. - * - * @param payload - A webhooks payload. - * @return a boolean to signify the success or failure of the HTTP POST request. - */ - @SuppressFBWarnings("REC_CATCH_EXCEPTION") - @Override - public boolean postWebhook(String payload) { - logger.fine("Sending webhook"); - - if(!evpProxySupported){ - logger.severe("Trying to send a webhook but the Agent doesn't support it."); - return false; - } - - DatadogGlobalConfiguration datadogGlobalDescriptor = DatadogUtilities.getDatadogGlobalDescriptor(); - String urlParameters = datadogGlobalDescriptor != null ? "?service=" + datadogGlobalDescriptor.getCiInstanceName() : ""; - String url = String.format("http://%s:%d/evp_proxy/v1/api/v2/webhook/%s", hostname, traceCollectionPort, urlParameters); - - Map headers = new HashMap<>(); - headers.put("X-Datadog-EVP-Subdomain", "webhook-intake"); - headers.put("DD-CI-PROVIDER-NAME", "jenkins"); - - byte[] body = payload.getBytes(StandardCharsets.UTF_8); - - try { - client.postAsynchronously(url, headers, "application/json", body); - return true; - } catch (Exception e) { - DatadogUtilities.severe(logger, e, "Error while posting webhook"); - return false; - } - } - - /** - * reinitialize the Tracer Client - * @param force - force to reinitialize - * @return true if reinitialized properly otherwise false - */ - protected boolean reinitializeAgentHttpClient(boolean force) { - if(!this.isStoppedAgentHttpClient && this.traceBuildLogic != null && this.tracePipelineLogic != null && !force) { - return true; - } - - if(!DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility() || this.getHostname() == null || this.getTraceCollectionPort() == null) { - return false; - } - - this.stopAgentHttpClient(); - try { - logger.info("Re/Initialize Datadog-Plugin Agent Http Client"); - - // Build - final URL tracesURL = buildHttpURL(this.getHostname(), this.getTraceCollectionPort(), "/v0.3/traces"); - this.agentHttpClient = NonBlockingHttpClient.builder() - .errorHandler(LOGGER_HTTP_ERROR_HANDLER) - .messageRoute(PayloadMessage.Type.TRACE, HttpMessageFactory.builder() - .agentURL(tracesURL) - .httpMethod(HttpMessage.HttpMethod.PUT) - .payloadMapper(new JsonTraceSpanMapper()) - .build()) - .build(); - - this.isStoppedAgentHttpClient = false; - return true; - } catch (Throwable e){ - DatadogUtilities.severe(logger, e, "Failed to reinitialize Datadog-Plugin Agent Http Client"); - this.stopAgentHttpClient(); - return false; - } - } - - protected boolean checkEvpProxySupportAndUpdateLogic() { - if (evpProxySupported) { - return true; // Once we have seen an Agent that supports EVP Proxy, we never check again. - } - if (System.currentTimeMillis() < (lastEvpProxyCheckTimeMs + EVP_PROXY_SUPPORT_TIME_BETWEEN_CHECKS_MS)) { - return evpProxySupported; // Wait at least 1 hour between checks, return the cached value - } - synchronized (DatadogAgentClient.class) { - if (!evpProxySupported) { - logger.info("Checking for EVP Proxy support in the Agent."); - Set supportedAgentEndpoints = fetchAgentSupportedEndpoints(); - evpProxySupported = supportedAgentEndpoints.contains("/evp_proxy/v3/"); - lastEvpProxyCheckTimeMs = System.currentTimeMillis(); - if (evpProxySupported) { - logger.info("EVP Proxy is supported by the Agent. We will not check again until the next boot."); - traceBuildLogic = new DatadogWebhookBuildLogic(this); - tracePipelineLogic = new DatadogWebhookPipelineLogic(this); - } else { - logger.info("The Agent doesn't support EVP Proxy, falling back to APM for CI Visibility. Requires Agent v6.42+ or 7.42+."); - traceBuildLogic = new DatadogTraceBuildLogic(this.agentHttpClient); - tracePipelineLogic = new DatadogTracePipelineLogic(this.agentHttpClient); - } - } - } - return evpProxySupported; - } - - private boolean stopAgentHttpClient() { - if(agentHttpClient != null) { - try { - this.agentHttpClient.stop(); - } catch (Throwable e) { - DatadogUtilities.severe(logger, e, "Failed to stop Agent Http Client"); - return false; - } - this.agentHttpClient = null; - } - this.isStoppedAgentHttpClient = true; - return true; - } - private boolean stopStatsDClient(){ if (this.statsd != null){ try{ @@ -498,87 +356,18 @@ public String getHostname() { return hostname; } - @Override - public void setHostname(String hostname) { - this.hostname = hostname; - } - public Integer getPort() { return port; } - @Override - public void setPort(Integer port) { - this.port = port; - } - public Integer getLogCollectionPort() { return logCollectionPort; } - @Override - public void setLogCollectionPort(Integer logCollectionPort) { - this.logCollectionPort = logCollectionPort; - } - public Integer getTraceCollectionPort() { return traceCollectionPort; } - @Override - public void setUrl(String url) { - // noop - } - - @Override - public void setLogIntakeUrl(String logIntakeUrl) { - // noop - } - - @Override - public void setWebhookIntakeUrl(String webhookIntakeUrl) { - // noop - } - - @Override - public void setApiKey(Secret apiKey){ - // noop - } - - @Override - public boolean isDefaultIntakeConnectionBroken() { - return false; - } - - @Override - public void setDefaultIntakeConnectionBroken(boolean defaultIntakeConnectionBroken) { - // noop - } - - @Override - public boolean isLogIntakeConnectionBroken() { - return false; - } - - @Override - public void setLogIntakeConnectionBroken(boolean logIntakeConnectionBroken) { - // noop - } - - @Override - public boolean isWebhookIntakeConnectionBroken() { - return false; - } - - @Override - public void setWebhookIntakeConnectionBroken(boolean webhookIntakeConnectionBroken) { - // noop - } - - public boolean isEvpProxySupported() { - return evpProxySupported; - } - @Override public boolean event(DatadogEvent event) { try { @@ -734,63 +523,68 @@ public boolean sendLogs(String payload) { } @Override - public boolean startBuildTrace(BuildData buildData, Run run) { - try { - boolean status = reinitializeAgentHttpClient(false); - if(!status) { - return false; - } + public TraceWriteStrategy createTraceWriteStrategy() { + TraceWriteStrategyImpl evpStrategy = new TraceWriteStrategyImpl(new DatadogWebhookBuildLogic(), new DatadogWebhookPipelineLogic(), this::sendSpansToWebhook); + TraceWriteStrategyImpl apmStrategy = new TraceWriteStrategyImpl(new DatadogTraceBuildLogic(), new DatadogTracePipelineLogic(), this::sendSpansToApm); + return new AgentTraceWriteStrategy(evpStrategy, apmStrategy, this::isEvpProxySupported); + } - checkEvpProxySupportAndUpdateLogic(); + boolean isEvpProxySupported() { + logger.info("Checking for EVP Proxy support in the Agent."); + Set supportedAgentEndpoints = fetchAgentSupportedEndpoints(); + return supportedAgentEndpoints.contains("/evp_proxy/v3/"); + } - logger.fine("Started build trace"); - this.traceBuildLogic.startBuildTrace(buildData, run); - return true; - } catch (Exception e) { - DatadogUtilities.severe(logger, e, "Failed to start build trace"); - reinitializeAgentHttpClient(true); - return false; + /** + * Posts a given payload to the Agent EVP Proxy, so it is forwarded to the Webhook Intake. + */ + private void sendSpansToWebhook(List spans) { + for (net.sf.json.JSONObject span : spans) { + // webhook intake does not support batch requests + postWebhook(span.toString()); } } + /** + * Posts a given payload to the Agent EVP Proxy, so it is forwarded to the Webhook Intake. + */ + private void postWebhook(String payload) { + logger.fine("Sending webhook"); - @Override - public boolean finishBuildTrace(BuildData buildData, Run run) { - try { - boolean status = reinitializeAgentHttpClient(false); - if(!status) { - return false; - } + DatadogGlobalConfiguration datadogGlobalDescriptor = DatadogUtilities.getDatadogGlobalDescriptor(); + String urlParameters = datadogGlobalDescriptor != null ? "?service=" + datadogGlobalDescriptor.getCiInstanceName() : ""; + String url = String.format("http://%s:%d/evp_proxy/v1/api/v2/webhook/%s", hostname, traceCollectionPort, urlParameters); - checkEvpProxySupportAndUpdateLogic(); + Map headers = new HashMap<>(); + headers.put("X-Datadog-EVP-Subdomain", "webhook-intake"); + headers.put("DD-CI-PROVIDER-NAME", "jenkins"); - logger.fine("Finished build trace"); - this.traceBuildLogic.finishBuildTrace(buildData, run); - return true; - } catch (Exception e) { - DatadogUtilities.severe(logger, e, "Failed to finish build trace"); - reinitializeAgentHttpClient(true); - return false; - } + byte[] body = payload.getBytes(StandardCharsets.UTF_8); + client.postAsynchronously(url, headers, "application/json", body); } - @Override - public boolean sendPipelineTrace(Run run, FlowNode flowNode) { + private void sendSpansToApm(List spans) { try { - boolean status = reinitializeAgentHttpClient(false); - if(!status) { - return false; + Map tracesById = new HashMap<>(); + for (net.sf.json.JSONObject span : spans) { + tracesById.computeIfAbsent(span.getString(JsonTraceSpanMapper.TRACE_ID), k -> new net.sf.json.JSONArray()).add(span); } - checkEvpProxySupportAndUpdateLogic(); + final JSONArray jsonTraces = new JSONArray(); + for(net.sf.json.JSONArray trace : tracesById.values()) { + jsonTraces.put(trace); + } + byte[] payload = jsonTraces.toString().getBytes(StandardCharsets.UTF_8); - logger.fine("Send pipeline traces."); - this.tracePipelineLogic.execute(run, flowNode); - return true; - } catch (Exception e){ - DatadogUtilities.severe(logger, e, "Failed to send pipeline trace"); - reinitializeAgentHttpClient(true); - return false; + String tracesUrl = String.format("http://%s:%d/v0.3/traces", hostname, port); + client.put(tracesUrl, Collections.emptyMap(), "application/json", payload, Function.identity()); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while sending trace", e); + + } catch (Exception e) { + throw new RuntimeException("Error while sending trace", e); } } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogHttpClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogApiClient.java similarity index 75% rename from src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogHttpClient.java rename to src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogApiClient.java index 9c76b3b8e..e67ccec68 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogHttpClient.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogApiClient.java @@ -25,12 +25,12 @@ of this software and associated documentation files (the "Software"), to deal package org.datadog.jenkins.plugins.datadog.clients; -import hudson.model.Run; import hudson.util.Secret; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; @@ -45,25 +45,25 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.DatadogEvent; import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; -import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookBuildLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookPipelineLogic; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategyImpl; import org.datadog.jenkins.plugins.datadog.util.SuppressFBWarnings; import org.datadog.jenkins.plugins.datadog.util.TagsUtil; -import org.jenkinsci.plugins.workflow.graph.FlowNode; /** * This class is used to collect all methods that has to do with transmitting * data to Datadog. */ -public class DatadogHttpClient implements DatadogClient { +public class DatadogApiClient implements DatadogClient { - private static volatile DatadogHttpClient instance = null; + private static volatile DatadogApiClient instance = null; // Used to determine if the instance failed last validation last time, so // we do not keep retrying to create the instance and logging the same error private static boolean failedLastValidation = false; - private static final Logger logger = Logger.getLogger(DatadogHttpClient.class.getName()); + private static final Logger logger = Logger.getLogger(DatadogApiClient.class.getName()); private static final String EVENT = "v1/events"; private static final String METRIC = "v1/series"; @@ -80,15 +80,13 @@ public class DatadogHttpClient implements DatadogClient { @SuppressFBWarnings(value="MS_SHOULD_BE_FINAL") public static boolean enableValidations = true; - private String url = null; - private String logIntakeUrl = null; - private String webhookIntakeUrl = null; - private Secret apiKey = null; + private final String url; + private final String logIntakeUrl; + private final String webhookIntakeUrl; + private final Secret apiKey; private boolean defaultIntakeConnectionBroken = false; private boolean logIntakeConnectionBroken = false; private boolean webhookIntakeConnectionBroken = false; - private DatadogWebhookBuildLogic webhookBuildLogic; - private DatadogWebhookPipelineLogic webhookPipelineLogic; private final HttpClient httpClient; @@ -105,22 +103,22 @@ public static DatadogClient getInstance(String url, String logIntakeUrl, String // If the configuration has not changed, return the current instance without validation // since we've already validated and/or errored about the data - DatadogHttpClient newInstance = new DatadogHttpClient(url, logIntakeUrl, webhookIntakeUrl, apiKey); + DatadogApiClient newInstance = new DatadogApiClient(url, logIntakeUrl, webhookIntakeUrl, apiKey); if (instance != null && instance.equals(newInstance)) { - if (DatadogHttpClient.failedLastValidation) { + if (DatadogApiClient.failedLastValidation) { return null; } return instance; } if (enableValidations) { - synchronized (DatadogHttpClient.class) { - DatadogHttpClient.instance = newInstance; + synchronized (DatadogApiClient.class) { + DatadogApiClient.instance = newInstance; try { newInstance.validateConfiguration(); - DatadogHttpClient.failedLastValidation = false; + DatadogApiClient.failedLastValidation = false; } catch(IllegalArgumentException e){ logger.severe(e.getMessage()); - DatadogHttpClient.failedLastValidation = true; + DatadogApiClient.failedLastValidation = true; return null; } } @@ -128,13 +126,11 @@ public static DatadogClient getInstance(String url, String logIntakeUrl, String return newInstance; } - private DatadogHttpClient(String url, String logIntakeUrl, String webhookIntakeUrl, Secret apiKey) { + private DatadogApiClient(String url, String logIntakeUrl, String webhookIntakeUrl, Secret apiKey) { this.url = url; this.apiKey = apiKey; this.logIntakeUrl = logIntakeUrl; this.webhookIntakeUrl = webhookIntakeUrl; - this.webhookBuildLogic = new DatadogWebhookBuildLogic(this); - this.webhookPipelineLogic = new DatadogWebhookPipelineLogic(this); this.httpClient = new HttpClient(HTTP_TIMEOUT_MS); } @@ -152,11 +148,11 @@ public void validateConfiguration() throws IllegalArgumentException { try { boolean logConnection = validateLogIntakeConnection(); if (!logConnection) { - instance.setLogIntakeConnectionBroken(true); + this.logIntakeConnectionBroken = true; logger.warning("Connection broken, please double check both your Log Intake URL and Key"); } } catch (IOException e) { - instance.setLogIntakeConnectionBroken(true); + this.logIntakeConnectionBroken = true; logger.warning("Connection broken, please double check both your Log Intake URL and Key: " + e); } } @@ -168,18 +164,18 @@ public void validateConfiguration() throws IllegalArgumentException { try { boolean webhookConnection = validateWebhookIntakeConnection(); if (!webhookConnection) { - instance.setWebhookIntakeConnectionBroken(true); + this.webhookIntakeConnectionBroken = true; logger.warning("Connection broken, please double check both your Webhook Intake URL and Key"); } } catch (IOException e) { - instance.setWebhookIntakeConnectionBroken(true); + this.webhookIntakeConnectionBroken = true; logger.warning("Connection broken, please double check both your Webhook Intake URL and Key: " + e); } } boolean intakeConnection = validateDefaultIntakeConnection(httpClient, url, apiKey); if (!intakeConnection) { - instance.setDefaultIntakeConnectionBroken(true); + this.defaultIntakeConnectionBroken = true; throw new IllegalArgumentException("Connection broken, please double check both your API URL and Key"); } } @@ -189,11 +185,11 @@ public boolean equals(Object object) { if (object == this) { return true; } - if (!(object instanceof DatadogHttpClient)) { + if (!(object instanceof DatadogApiClient)) { return false; } - DatadogHttpClient newInstance = (DatadogHttpClient) object; + DatadogApiClient newInstance = (DatadogApiClient) object; return StringUtils.equals(getLogIntakeUrl(), newInstance.getLogIntakeUrl()) && StringUtils.equals(getWebhookIntakeUrl(), newInstance.getWebhookIntakeUrl()) @@ -214,86 +210,21 @@ public String getUrl() { return url; } - @Override - public void setUrl(String url) { - this.url = url; - } - public String getLogIntakeUrl() { return logIntakeUrl; } - @Override - public void setLogIntakeUrl(String logIntakeUrl) { - this.logIntakeUrl = logIntakeUrl; - } - public String getWebhookIntakeUrl() { return webhookIntakeUrl; } - @Override - public void setWebhookIntakeUrl(String webhookIntakeUrl) { - this.webhookIntakeUrl = webhookIntakeUrl; - } - public Secret getApiKey() { return apiKey; } - @Override - public void setApiKey(Secret apiKey) { - this.apiKey = apiKey; - } - - @Override - public void setHostname(String hostname) { - // noop - } - - @Override - public void setPort(Integer port) { - // noop - } - - @Override - public void setLogCollectionPort(Integer logCollectionPort) { - // noop - } - - @Override - public boolean isDefaultIntakeConnectionBroken() { - return defaultIntakeConnectionBroken; - } - - @Override - public void setDefaultIntakeConnectionBroken(boolean defaultIntakeConnectionBroken) { - this.defaultIntakeConnectionBroken = defaultIntakeConnectionBroken; - } - - @Override - public boolean isLogIntakeConnectionBroken() { - return logIntakeConnectionBroken; - } - - @Override - public void setLogIntakeConnectionBroken(boolean logIntakeConnectionBroken) { - this.logIntakeConnectionBroken = logIntakeConnectionBroken; - } - - @Override - public boolean isWebhookIntakeConnectionBroken() { - return webhookIntakeConnectionBroken; - } - - @Override - public void setWebhookIntakeConnectionBroken(boolean webhookIntakeConnectionBroken) { - this.webhookIntakeConnectionBroken = webhookIntakeConnectionBroken; - } - public boolean event(DatadogEvent event) { logger.fine("Sending event"); - if(this.isDefaultIntakeConnectionBroken()){ + if(this.defaultIntakeConnectionBroken){ logger.severe("Your client is not initialized properly"); return false; } @@ -319,7 +250,7 @@ public boolean event(DatadogEvent event) { @Override public boolean incrementCounter(String name, String hostname, Map> tags) { - if(this.isDefaultIntakeConnectionBroken()){ + if(this.defaultIntakeConnectionBroken){ logger.severe("Your client is not initialized properly"); return false; } @@ -449,7 +380,7 @@ public boolean serviceCheck(String name, Status status, String hostname, Map headers = new HashMap<>(); - headers.put("DD-API-KEY", Secret.toString(apiKey)); - headers.put("DD-CI-PROVIDER-NAME", "jenkins"); - - byte[] body = payload.getBytes(StandardCharsets.UTF_8); - - try { - httpClient.postAsynchronously(url, headers, "application/json", body); - return true; - } catch (Exception e) { - DatadogUtilities.severe(logger, e, "Failed to post webhook"); - return false; - } - } - public static boolean validateDefaultIntakeConnection(HttpClient client, String validatedUrl, Secret apiKey) { String urlParameters = "?api_key=" + Secret.toString(apiKey); String url = validatedUrl + VALIDATE + urlParameters; @@ -557,14 +454,14 @@ public static boolean validateDefaultIntakeConnection(HttpClient client, String } } - public boolean validateLogIntakeConnection() throws IOException { + private boolean validateLogIntakeConnection() throws IOException { return postLogs("{\"message\":\"[datadog-plugin] Check connection\", " + "\"ddsource\":\"Jenkins\", \"service\":\"Jenkins\", " + "\"hostname\":\""+DatadogUtilities.getHostname(null)+"\"}"); } @SuppressFBWarnings("DLS_DEAD_LOCAL_STORE") - public boolean validateWebhookIntakeConnection() throws IOException { + private boolean validateWebhookIntakeConnection() throws IOException { String url = getWebhookIntakeUrl(); Map headers = new HashMap<>(); @@ -587,50 +484,38 @@ public boolean validateWebhookIntakeConnection() throws IOException { } @Override - public boolean startBuildTrace(BuildData buildData, Run run) { - if(this.isWebhookIntakeConnectionBroken()){ - logger.severe("Unable to start build trace; your client is not initialized properly."); - return false; - } - try { - logger.fine("Started build trace"); - this.webhookBuildLogic.startBuildTrace(buildData, run); - return true; - } catch (Exception e) { - DatadogUtilities.severe(logger, e, "Failed to start build trace"); - return false; - } + public TraceWriteStrategy createTraceWriteStrategy() { + return new TraceWriteStrategyImpl(new DatadogWebhookBuildLogic(), new DatadogWebhookPipelineLogic(), this::sendSpans); } - @Override - public boolean finishBuildTrace(BuildData buildData, Run run) { - if(this.isWebhookIntakeConnectionBroken()){ - logger.severe("Unable to finish build trace; your client is not initialized properly."); - return false; - } - try { - logger.fine("Finished build trace"); - this.webhookBuildLogic.finishBuildTrace(buildData, run); - return true; - } catch (Exception e) { - DatadogUtilities.severe(logger, e, "Failed to finish build trace"); - return false; + private void sendSpans(List spans) { + for (JSONObject span : spans) { + // webhook intake does not support batch requests + postWebhook(span.toString()); } } - @Override - public boolean sendPipelineTrace(Run run, FlowNode flowNode) { - if(this.isWebhookIntakeConnectionBroken()){ - logger.severe("Unable to send pipeline trace; your client is not initialized properly"); - return false; - } - try { - logger.fine("Send pipeline traces."); - this.webhookPipelineLogic.execute(run, flowNode); - return true; - } catch (Exception e) { - DatadogUtilities.severe(logger, e, "Failed to send pipeline trace"); - return false; + /** + * Posts a given payload to the Datadog Webhook Intake, using the user configured apiKey. + * + * @param payload - A webhook payload. + */ + private void postWebhook(String payload) { + logger.fine("Sending webhook"); + + if (this.webhookIntakeConnectionBroken) { + throw new RuntimeException("Your client is not initialized properly; webhook intake connection is broken."); } + + DatadogGlobalConfiguration datadogGlobalDescriptor = DatadogUtilities.getDatadogGlobalDescriptor(); + String urlParameters = datadogGlobalDescriptor != null ? "?service=" + datadogGlobalDescriptor.getCiInstanceName() : ""; + String url = getWebhookIntakeUrl() + urlParameters; + + Map headers = new HashMap<>(); + headers.put("DD-API-KEY", Secret.toString(apiKey)); + headers.put("DD-CI-PROVIDER-NAME", "jenkins"); + + byte[] body = payload.getBytes(StandardCharsets.UTF_8); + httpClient.postAsynchronously(url, headers, "application/json", body); } } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/HttpClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/HttpClient.java index e72babe77..a2604a974 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/HttpClient.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/HttpClient.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; @@ -19,7 +18,6 @@ import java.util.regex.Pattern; import jenkins.model.Jenkins; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; -import org.datadog.jenkins.plugins.datadog.transport.HttpMessage; import org.eclipse.jetty.client.HttpProxy; import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.ProxyConfiguration; @@ -204,6 +202,13 @@ public void getBinary(String url, Map headers, Consumer T put(String url, Map headers, String contentType, byte[] body, Function responseParser) throws ExecutionException, InterruptedException, TimeoutException { + return executeSynchronously( + requestSupplier(url, HttpMethod.PUT, headers, contentType, body), + retryPolicyFactory.create(), + responseParser); + } + public T post(String url, Map headers, String contentType, byte[] body, Function responseParser) throws ExecutionException, InterruptedException, TimeoutException { return executeSynchronously( requestSupplier(url, HttpMethod.POST, headers, contentType, body), @@ -223,22 +228,6 @@ public void postAsynchronously(String url, Map headers, String c ); } - public void sendAsynchronously(HttpMessage message) { - String url = message.getURL().toString(); - HttpMethod httpMethod = HttpMethod.fromString(message.getMethod().name()); - String contentType = message.getContentType(); - byte[] body = message.getPayload(); - executeAsynchronously( - requestSupplier( - url, - httpMethod, - Collections.emptyMap(), - contentType, - body), - retryPolicyFactory.create() - ); - } - private Supplier requestSupplier(String url, HttpMethod method, Map headers, String contentType, byte[] body) { return () -> { Request request = CLIENT diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListener.java b/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListener.java index 9b6dba828..10ecf4d2c 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListener.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListener.java @@ -58,11 +58,16 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.events.BuildFinishedEventImpl; import org.datadog.jenkins.plugins.datadog.events.BuildStartedEventImpl; import org.datadog.jenkins.plugins.datadog.model.BuildData; +import org.datadog.jenkins.plugins.datadog.model.CIGlobalTagsAction; +import org.datadog.jenkins.plugins.datadog.model.PipelineQueueInfoAction; +import org.datadog.jenkins.plugins.datadog.model.StageBreakdownAction; import org.datadog.jenkins.plugins.datadog.traces.BuildSpanAction; - import org.datadog.jenkins.plugins.datadog.traces.BuildSpanManager; import org.datadog.jenkins.plugins.datadog.traces.StepDataAction; +import org.datadog.jenkins.plugins.datadog.traces.StepTraceDataAction; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriter; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriterFactory; import org.jenkinsci.plugins.workflow.job.WorkflowRun; @@ -123,13 +128,32 @@ public void onInitialize(Run run) { run.addAction(stepDataAction); // Traces - client.startBuildTrace(buildData, run); + startBuildTrace(buildData, run); logger.fine("End DatadogBuildListener#onInitialize"); } catch (Exception e) { DatadogUtilities.severe(logger, e, "Failed to process build initialization"); } } + private void startBuildTrace(final BuildData buildData, Run run) { + if (!DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility()) { + logger.fine("CI Visibility is disabled"); + return; + } + + final StepTraceDataAction stepTraceDataAction = new StepTraceDataAction(); + run.addAction(stepTraceDataAction); + + final StageBreakdownAction stageBreakdownAction = new StageBreakdownAction(); + run.addAction(stageBreakdownAction); + + final PipelineQueueInfoAction pipelineQueueInfoAction = new PipelineQueueInfoAction(); + run.addAction(pipelineQueueInfoAction); + + final CIGlobalTagsAction ciGlobalTags = new CIGlobalTagsAction(buildData.getTagsForTraces()); + run.addAction(ciGlobalTags); + } + /** * Called before the SCMCheckout is run in a Jenkins build. * This method is called after onInitialize callback. @@ -365,9 +389,8 @@ public void onFinalized(Run run) { } logger.fine("Start DatadogBuildListener#onFinalized"); - // Get Datadog Client Instance - DatadogClient client = getDatadogClient(); - if (client == null) { + TraceWriter traceWriter = TraceWriterFactory.getTraceWriter(); + if (traceWriter == null) { return; } @@ -381,11 +404,14 @@ public void onFinalized(Run run) { } // APM Traces - client.finishBuildTrace(buildData, run); + traceWriter.submitBuild(buildData, run); logger.fine("End DatadogBuildListener#onFinalized"); BuildSpanManager.get().remove(buildData.getBuildTag("")); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + DatadogUtilities.severe(logger, e, "Interrupted while processing build finalization"); } catch (Exception e) { DatadogUtilities.severe(logger, e, "Failed to process build finalization"); } finally { diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListener.java b/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListener.java index a6e8f6f9c..226216c80 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListener.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListener.java @@ -65,6 +65,8 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.model.StageBreakdownAction; import org.datadog.jenkins.plugins.datadog.model.StageData; import org.datadog.jenkins.plugins.datadog.traces.BuildSpanAction; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriter; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriterFactory; import org.datadog.jenkins.plugins.datadog.util.TagsUtil; import org.datadog.jenkins.plugins.datadog.util.git.GitUtils; import org.jenkinsci.plugins.gitclient.GitClient; @@ -105,14 +107,23 @@ public void onNewHead(FlowNode flowNode) { } } - //APM Traces + TraceWriter traceWriter = TraceWriterFactory.getTraceWriter(); + if (traceWriter != null) { + try { + traceWriter.submitPipeline(flowNode, run); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + DatadogUtilities.severe(logger, e, "Interrupted while submitting pipeline trace for node " + flowNode.getDisplayName() + " in run " + (run != null ? run.getDisplayName() : "")); + } catch (Exception e) { + DatadogUtilities.severe(logger, e, "Error while submitting pipeline trace for node " + flowNode.getDisplayName() + " in run " + (run != null ? run.getDisplayName() : "")); + } + } + DatadogClient client = ClientFactory.getClient(); if (client == null){ return; } - client.sendPipelineTrace(run, flowNode); - if (!isMonitored(flowNode)) { return; } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogBaseBuildLogic.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogBaseBuildLogic.java index 0dad1d522..d5852d3d2 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogBaseBuildLogic.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogBaseBuildLogic.java @@ -1,5 +1,7 @@ package org.datadog.jenkins.plugins.datadog.traces; +import hudson.model.Cause; +import hudson.model.Run; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -7,7 +9,8 @@ import java.util.Map; import java.util.Set; import java.util.logging.Logger; - +import javax.annotation.Nullable; +import net.sf.json.JSONObject; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.datadog.jenkins.plugins.datadog.model.PipelineNodeInfoAction; @@ -15,8 +18,6 @@ import org.datadog.jenkins.plugins.datadog.model.StageData; import org.datadog.jenkins.plugins.datadog.util.SuppressFBWarnings; import org.datadog.jenkins.plugins.datadog.util.json.JsonUtils; -import hudson.model.Cause; -import hudson.model.Run; /** * Base class for DatadogTraceBuildLogic and DatadogPipelineBuildLogic @@ -27,8 +28,8 @@ public abstract class DatadogBaseBuildLogic { private static final int MAX_TAG_LENGTH = 5000; private static final Logger logger = Logger.getLogger(DatadogBaseBuildLogic.class.getName()); - public abstract void finishBuildTrace(final BuildData buildData, final Run run); - public abstract void startBuildTrace(final BuildData buildData, Run run); + @Nullable + public abstract JSONObject finishBuildTrace(final BuildData buildData, final Run run); protected String getNodeName(Run run, BuildData buildData, BuildData updatedBuildData) { final PipelineNodeInfoAction pipelineNodeInfoAction = run.getAction(PipelineNodeInfoAction.class); diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogBasePipelineLogic.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogBasePipelineLogic.java index a948f5947..54526597d 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogBasePipelineLogic.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogBasePipelineLogic.java @@ -1,12 +1,16 @@ package org.datadog.jenkins.plugins.datadog.traces; +import hudson.model.Run; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeoutException; import java.util.logging.Logger; - +import javax.annotation.Nonnull; +import net.sf.json.JSONObject; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; import org.datadog.jenkins.plugins.datadog.audit.DatadogAudit; import org.datadog.jenkins.plugins.datadog.model.BuildData; @@ -20,8 +24,6 @@ import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; -import hudson.model.Run; - /** * Base class with shared code for DatadogTracePipelineLogic and DatadogWebhookPipelineLogic */ @@ -31,7 +33,8 @@ public abstract class DatadogBasePipelineLogic { protected static final String HOSTNAME_NONE = "none"; private static final Logger logger = Logger.getLogger(DatadogBasePipelineLogic.class.getName()); - public abstract void execute(Run run, FlowNode flowNode); + @Nonnull + public abstract Collection execute(FlowNode flowNode, Run run); protected BuildPipelineNode buildPipelineTree(FlowEndNode flowEndNode) { diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogTraceBuildLogic.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogTraceBuildLogic.java index 25da2f87e..518abceda 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogTraceBuildLogic.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogTraceBuildLogic.java @@ -8,24 +8,20 @@ import static org.datadog.jenkins.plugins.datadog.traces.GitInfoUtils.normalizeTag; import static org.datadog.jenkins.plugins.datadog.util.git.GitUtils.isValidCommit; -import java.util.Collections; +import hudson.model.Result; +import hudson.model.Run; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; - +import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.datadog.jenkins.plugins.datadog.model.BuildPipelineNode; import org.datadog.jenkins.plugins.datadog.model.CIGlobalTagsAction; -import org.datadog.jenkins.plugins.datadog.model.PipelineQueueInfoAction; -import org.datadog.jenkins.plugins.datadog.model.StageBreakdownAction; +import org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; -import org.datadog.jenkins.plugins.datadog.transport.HttpClient; - -import hudson.model.Result; -import hudson.model.Run; /** * Keeps the logic to send traces related to Jenkins Build. @@ -35,52 +31,28 @@ public class DatadogTraceBuildLogic extends DatadogBaseBuildLogic { private static final Logger logger = Logger.getLogger(DatadogTraceBuildLogic.class.getName()); - private final HttpClient agentHttpClient; - - public DatadogTraceBuildLogic(final HttpClient agentHttpClient) { - this.agentHttpClient = agentHttpClient; - } + private final JsonTraceSpanMapper jsonTraceSpanMapper = new JsonTraceSpanMapper(); @Override - public void startBuildTrace(final BuildData buildData, Run run) { - if (!DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility()) { - logger.fine("CI Visibility is disabled"); - return; - } - - // Traces - if(this.agentHttpClient == null) { - logger.severe("Unable to send build traces. Tracer is null"); - return; - } - - final StepTraceDataAction stepTraceDataAction = new StepTraceDataAction(); - run.addAction(stepTraceDataAction); - - final StageBreakdownAction stageBreakdownAction = new StageBreakdownAction(); - run.addAction(stageBreakdownAction); - - final PipelineQueueInfoAction pipelineQueueInfoAction = new PipelineQueueInfoAction(); - run.addAction(pipelineQueueInfoAction); - - final CIGlobalTagsAction ciGlobalTags = new CIGlobalTagsAction(buildData.getTagsForTraces()); - run.addAction(ciGlobalTags); + public JSONObject finishBuildTrace(final BuildData buildData, final Run run) { + TraceSpan span = createSpan(buildData, run); + return span != null ? jsonTraceSpanMapper.map(span) : null; } - @Override - public void finishBuildTrace(final BuildData buildData, final Run run) { + // hook for tests + public TraceSpan createSpan(final BuildData buildData, final Run run) { if (!DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility()) { - return; + return null; } final TraceSpan buildSpan = BuildSpanManager.get().get(buildData.getBuildTag("")); if(buildSpan == null) { - return; + return null; } final BuildSpanAction buildSpanAction = run.getAction(BuildSpanAction.class); if(buildSpanAction == null) { - return; + return null; } // In this point of the execution, the BuildData stored within @@ -263,7 +235,8 @@ public void finishBuildTrace(final BuildData buildData, final Run run) { // When the root span starts, we don't have the propagated queue time yet. We need to wait till the // end of the pipeline execution and do it in the endTime, adjusting all child spans if needed. buildSpan.setEndNano(TimeUnit.MICROSECONDS.toNanos(endTimeMicros - TimeUnit.MILLISECONDS.toMicros(propagatedMillisInQueue))); - agentHttpClient.send(Collections.singletonList(buildSpan)); + + return buildSpan; } } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogTracePipelineLogic.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogTracePipelineLogic.java index f2956702c..cc6ae4596 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogTracePipelineLogic.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogTracePipelineLogic.java @@ -9,29 +9,29 @@ import static org.datadog.jenkins.plugins.datadog.traces.GitInfoUtils.normalizeBranch; import static org.datadog.jenkins.plugins.datadog.traces.GitInfoUtils.normalizeTag; +import hudson.model.Run; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.datadog.jenkins.plugins.datadog.model.BuildPipelineNode; import org.datadog.jenkins.plugins.datadog.model.CIGlobalTagsAction; +import org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; -import org.datadog.jenkins.plugins.datadog.transport.HttpClient; -import org.datadog.jenkins.plugins.datadog.transport.PayloadMessage; import org.datadog.jenkins.plugins.datadog.util.git.GitUtils; import org.jenkinsci.plugins.workflow.graph.FlowEndNode; import org.jenkinsci.plugins.workflow.graph.FlowNode; -import hudson.model.Run; - /** * Keeps the logic to send traces related to inner jobs of Jenkins Pipelines (datadog levels: stage and job). @@ -39,23 +39,19 @@ */ public class DatadogTracePipelineLogic extends DatadogBasePipelineLogic { - private static final Logger logger = Logger.getLogger(DatadogTracePipelineLogic.class.getName()); - - private final HttpClient agentHttpClient; + private final JsonTraceSpanMapper jsonTraceSpanMapper = new JsonTraceSpanMapper(); - public DatadogTracePipelineLogic(HttpClient agentHttpClient) { - this.agentHttpClient = agentHttpClient; + @Nonnull + @Override + public Collection execute(FlowNode flowNode, Run run) { + Collection traces = collectTraces(flowNode, run); + return traces.stream().map(jsonTraceSpanMapper::map).collect(Collectors.toList()); } - @Override - public void execute(Run run, FlowNode flowNode) { + // hook for tests + public Collection collectTraces(FlowNode flowNode, Run run) { if (!DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility()) { - return; - } - - if(this.agentHttpClient == null) { - logger.severe("Unable to send pipeline traces. Tracer is null"); - return; + return Collections.emptySet(); } final IsPipelineAction isPipelineAction = run.getAction(IsPipelineAction.class); @@ -65,40 +61,34 @@ public void execute(Run run, FlowNode flowNode) { final BuildSpanAction buildSpanAction = run.getAction(BuildSpanAction.class); if(buildSpanAction == null) { - return; + return Collections.emptySet(); } final BuildData buildData = buildSpanAction.getBuildData(); if(!DatadogUtilities.isLastNode(flowNode)){ updateCIGlobalTags(run); - return; + return Collections.emptySet(); } final TraceSpan.TraceSpanContext traceSpanContext = buildSpanAction.getBuildSpanContext(); final BuildPipelineNode root = buildPipelineTree((FlowEndNode) flowNode); - final List spanBuffer = new ArrayList<>(); - collectTraces(run, spanBuffer, buildData, root, traceSpanContext); - try { - if(!spanBuffer.isEmpty()) { - this.agentHttpClient.send(spanBuffer); - } - } catch (Exception e){ - logger.severe("Unable to send traces. Exception:" + e); + return collectTraces(run, buildData, root, traceSpanContext); } finally { // Explicit removal of InvisibleActions used to collect Traces when the Run finishes. cleanUpTraceActions(run); } } - private void collectTraces(final Run run, final List spanBuffer, final BuildData buildData, final BuildPipelineNode current, final TraceSpan.TraceSpanContext parentSpanContext) { + private Collection collectTraces(final Run run, final BuildData buildData, final BuildPipelineNode current, final TraceSpan.TraceSpanContext parentSpanContext) { if(!isTraceable(current)) { + Collection traces = new ArrayList<>(); // If the current node is not traceable, we continue with its children for(final BuildPipelineNode child : current.getChildren()) { - collectTraces(run, spanBuffer, buildData, child, parentSpanContext); + traces.addAll(collectTraces(run, buildData, child, parentSpanContext)); } - return; + return traces; } // If the root span has propagated queue time, we need to adjust all startTime and endTime from Jenkins pipelines spans @@ -136,8 +126,9 @@ private void collectTraces(final Run run, final List spanBuffer, } } + Collection traces = new ArrayList<>(); for(final BuildPipelineNode child : current.getChildren()) { - collectTraces(run, spanBuffer, buildData, child, span.context()); + traces.addAll(collectTraces(run, buildData, child, span.context())); } //Logs @@ -145,7 +136,8 @@ private void collectTraces(final Run run, final List spanBuffer, span.setEndNano(fixedEndTimeNanos); - spanBuffer.add(span); + traces.add(span); + return traces; } private Map buildTraceMetrics(BuildPipelineNode current) { diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogWebhookBuildLogic.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogWebhookBuildLogic.java index c2ecb4591..6e0cc9313 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogWebhookBuildLogic.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogWebhookBuildLogic.java @@ -6,27 +6,21 @@ import static org.datadog.jenkins.plugins.datadog.traces.GitInfoUtils.normalizeTag; import static org.datadog.jenkins.plugins.datadog.util.git.GitUtils.isValidCommit; +import hudson.model.Run; import java.util.Date; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.logging.Logger; - +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; -import org.datadog.jenkins.plugins.datadog.DatadogClient; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.datadog.jenkins.plugins.datadog.model.BuildPipelineNode; import org.datadog.jenkins.plugins.datadog.model.CIGlobalTagsAction; -import org.datadog.jenkins.plugins.datadog.model.PipelineQueueInfoAction; -import org.datadog.jenkins.plugins.datadog.model.StageBreakdownAction; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; -import hudson.model.Run; -import net.sf.json.JSONArray; -import net.sf.json.JSONObject; - /** * Keeps the logic to send webhooks related to Jenkins Build. * This gets called once per job (datadog level: pipeline) @@ -35,46 +29,20 @@ public class DatadogWebhookBuildLogic extends DatadogBaseBuildLogic { private static final Logger logger = Logger.getLogger(DatadogWebhookBuildLogic.class.getName()); - private final DatadogClient client; - - public DatadogWebhookBuildLogic(final DatadogClient client) { - this.client = client; - } - - @Override - public void startBuildTrace(final BuildData buildData, Run run) { - if (!DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility()) { - logger.fine("CI Visibility is disabled"); - return; - } - - final StepTraceDataAction stepTraceDataAction = new StepTraceDataAction(); - run.addAction(stepTraceDataAction); - - final StageBreakdownAction stageBreakdownAction = new StageBreakdownAction(); - run.addAction(stageBreakdownAction); - - final PipelineQueueInfoAction pipelineQueueInfoAction = new PipelineQueueInfoAction(); - run.addAction(pipelineQueueInfoAction); - - final CIGlobalTagsAction ciGlobalTags = new CIGlobalTagsAction(buildData.getTagsForTraces()); - run.addAction(ciGlobalTags); - } - @Override - public void finishBuildTrace(final BuildData buildData, final Run run) { + public JSONObject finishBuildTrace(final BuildData buildData, final Run run) { if (!DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility()) { - return; + return null; } final TraceSpan buildSpan = BuildSpanManager.get().get(buildData.getBuildTag("")); if(buildSpan == null) { - return; + return null; } final BuildSpanAction buildSpanAction = run.getAction(BuildSpanAction.class); if(buildSpanAction == null) { - return; + return null; } // In this point of the execution, the BuildData stored within @@ -285,7 +253,7 @@ public void finishBuildTrace(final BuildData buildData, final Run run) { payload.put("git", gitPayload); } - client.postWebhook(payload.toString()); + return payload; } } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogWebhookPipelineLogic.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogWebhookPipelineLogic.java index 7afd20659..9d5e641e2 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogWebhookPipelineLogic.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/DatadogWebhookPipelineLogic.java @@ -6,15 +6,20 @@ import static org.datadog.jenkins.plugins.datadog.traces.GitInfoUtils.normalizeBranch; import static org.datadog.jenkins.plugins.datadog.traces.GitInfoUtils.normalizeTag; +import hudson.model.Run; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; - +import javax.annotation.Nonnull; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; -import org.datadog.jenkins.plugins.datadog.DatadogClient; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.datadog.jenkins.plugins.datadog.model.BuildPipelineNode; @@ -24,27 +29,18 @@ import org.jenkinsci.plugins.workflow.graph.FlowEndNode; import org.jenkinsci.plugins.workflow.graph.FlowNode; -import hudson.model.Run; -import net.sf.json.JSONArray; -import net.sf.json.JSONObject; - /** * Keeps the logic to send webhooks related to inner jobs of Jenkins Pipelines (datadog levels: stage and job). * The top-level job (datadog level: pipeline) is handled by DatadogWebhookBuildLogic */ public class DatadogWebhookPipelineLogic extends DatadogBasePipelineLogic { - private final DatadogClient client; - - public DatadogWebhookPipelineLogic(final DatadogClient client) { - this.client = client; - } - + @Nonnull @Override - public void execute(Run run, FlowNode flowNode) { + public Collection execute(FlowNode flowNode, Run run) { if (!DatadogUtilities.getDatadogGlobalDescriptor().getEnableCiVisibility()) { - return; + return Collections.emptySet(); } final IsPipelineAction isPipelineAction = run.getAction(IsPipelineAction.class); @@ -54,31 +50,34 @@ public void execute(Run run, FlowNode flowNode) { final BuildSpanAction buildSpanAction = run.getAction(BuildSpanAction.class); if(buildSpanAction == null) { - return; + return Collections.emptySet(); } final BuildData buildData = buildSpanAction.getBuildData(); if(!DatadogUtilities.isLastNode(flowNode)){ updateCIGlobalTags(run); - return; + return Collections.emptySet(); } final TraceSpan.TraceSpanContext traceSpanContext = buildSpanAction.getBuildSpanContext(); final BuildPipelineNode root = buildPipelineTree((FlowEndNode) flowNode); - collectTraces(run, buildData, root, null, traceSpanContext); - - // Explicit removal of InvisibleActions used to collect Traces when the Run finishes. - cleanUpTraceActions(run); + try { + return collectTraces(run, buildData, root, null, traceSpanContext); + } finally { + // Explicit removal of InvisibleActions used to collect Traces when the Run finishes. + cleanUpTraceActions(run); + } } - private void collectTraces(final Run run, final BuildData buildData, final BuildPipelineNode current, final BuildPipelineNode parent, final TraceSpan.TraceSpanContext parentSpanContext) { + private Collection collectTraces(final Run run, final BuildData buildData, final BuildPipelineNode current, final BuildPipelineNode parent, final TraceSpan.TraceSpanContext parentSpanContext) { if(!isTraceable(current)) { + Collection traces = new ArrayList<>(); // If the current node is not traceable, we continue with its children for(final BuildPipelineNode child : current.getChildren()) { - collectTraces(run, buildData, child, parent, parentSpanContext); + traces.addAll(collectTraces(run, buildData, child, parent, parentSpanContext)); } - return; + return traces; } // If the root has propagated queue time, we need to adjust all startTime and endTime from Jenkins pipelines // because this time will be subtracted in the root. See DatadogTraceBuildLogic#finishBuildTrace method. @@ -314,11 +313,13 @@ private void collectTraces(final Run run, final BuildData buildData, final Build payload.put("tags", tagsPayload); } + Collection traces = new ArrayList<>(); for(final BuildPipelineNode child : current.getChildren()) { - collectTraces(run, buildData, child, current, span.context()); + traces.addAll(collectTraces(run, buildData, child, current, span.context())); } - client.postWebhook(payload.toString()); + traces.add(payload); + return traces; } } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/mapper/JsonTraceSpanMapper.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/mapper/JsonTraceSpanMapper.java index dfdc9c9cb..bbe580841 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/mapper/JsonTraceSpanMapper.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/mapper/JsonTraceSpanMapper.java @@ -1,22 +1,16 @@ package org.datadog.jenkins.plugins.datadog.traces.mapper; -import net.sf.json.JSONArray; +import java.util.Map; import net.sf.json.JSONObject; -import org.datadog.jenkins.plugins.datadog.transport.PayloadMapper; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * PayloadMapper to transform TraceSpan into JSON object * following the spec: https://docs.datadoghq.com/api/latest/tracing/ */ -public class JsonTraceSpanMapper implements PayloadMapper> { +public class JsonTraceSpanMapper { - static final String TRACE_ID = "trace_id"; + public static final String TRACE_ID = "trace_id"; static final String SPAN_ID = "span_id"; static final String PARENT_ID = "parent_id"; @@ -31,61 +25,40 @@ public class JsonTraceSpanMapper implements PayloadMapper> { static final String START = "start"; static final String DURATION = "duration"; - @Override - public byte[] map(final List spans) { - final Map tracesMap = new HashMap<>(); - for(final TraceSpan span : spans) { - JSONArray jsonArray = tracesMap.get(span.context().getTraceId()); - if(jsonArray == null){ - jsonArray = new JSONArray(); - tracesMap.put(span.context().getTraceId(), jsonArray); - } - - final JSONObject jsonSpan = new JSONObject(); - jsonSpan.put(TRACE_ID, span.context().getTraceId()); - jsonSpan.put(SPAN_ID, span.context().getSpanId()); - if(span.context().getParentId() != 0){ - jsonSpan.put(PARENT_ID, span.context().getParentId()); - } - - if(span.isError()){ - jsonSpan.put(ERROR, 1); - } - - jsonSpan.put(OPERATION_NAME, span.getOperationName()); - jsonSpan.put(RESOURCE_NAME, span.getResourceName()); - jsonSpan.put(SERVICE_NAME, span.getServiceName()); - jsonSpan.put(SPAN_TYPE, span.getType()); - - final JSONObject jsonMeta = new JSONObject(); - final Map meta = span.getMeta(); - for(Map.Entry metaEntry : meta.entrySet()) { - jsonMeta.put(metaEntry.getKey(), metaEntry.getValue()); - } - jsonSpan.put(META, jsonMeta); + public JSONObject map(final TraceSpan span) { + final JSONObject jsonSpan = new JSONObject(); + jsonSpan.put(TRACE_ID, span.context().getTraceId()); + jsonSpan.put(SPAN_ID, span.context().getSpanId()); + if(span.context().getParentId() != 0){ + jsonSpan.put(PARENT_ID, span.context().getParentId()); + } - final JSONObject jsonMetrics = new JSONObject(); - final Map metrics = span.getMetrics(); - for(Map.Entry metric : metrics.entrySet()){ - jsonMetrics.put(metric.getKey(), metric.getValue()); - } - jsonSpan.put(METRICS, jsonMetrics); + if(span.isError()){ + jsonSpan.put(ERROR, 1); + } - jsonSpan.put(START, span.getStartNano()); - jsonSpan.put(DURATION, span.getDurationNano()); + jsonSpan.put(OPERATION_NAME, span.getOperationName()); + jsonSpan.put(RESOURCE_NAME, span.getResourceName()); + jsonSpan.put(SERVICE_NAME, span.getServiceName()); + jsonSpan.put(SPAN_TYPE, span.getType()); - jsonArray.add(jsonSpan); + final JSONObject jsonMeta = new JSONObject(); + final Map meta = span.getMeta(); + for(Map.Entry metaEntry : meta.entrySet()) { + jsonMeta.put(metaEntry.getKey(), metaEntry.getValue()); } + jsonSpan.put(META, jsonMeta); - final JSONArray jsonTraces = new JSONArray(); - for(Map.Entry traceEntry : tracesMap.entrySet()) { - jsonTraces.add(traceEntry.getValue()); + final JSONObject jsonMetrics = new JSONObject(); + final Map metrics = span.getMetrics(); + for(Map.Entry metric : metrics.entrySet()){ + jsonMetrics.put(metric.getKey(), metric.getValue()); } - return jsonTraces.toString().getBytes(StandardCharsets.UTF_8); - } + jsonSpan.put(METRICS, jsonMetrics); + + jsonSpan.put(START, span.getStartNano()); + jsonSpan.put(DURATION, span.getDurationNano()); - @Override - public String contentType() { - return "application/json"; + return jsonSpan; } } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/message/TraceSpan.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/message/TraceSpan.java index 3f9b4dbfb..062b6a188 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/message/TraceSpan.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/message/TraceSpan.java @@ -1,13 +1,11 @@ package org.datadog.jenkins.plugins.datadog.traces.message; -import org.datadog.jenkins.plugins.datadog.traces.IdGenerator; -import org.datadog.jenkins.plugins.datadog.transport.PayloadMessage; - import java.io.Serializable; import java.util.HashMap; import java.util.Map; +import org.datadog.jenkins.plugins.datadog.traces.IdGenerator; -public class TraceSpan implements PayloadMessage { +public class TraceSpan { public static final String PRIORITY_SAMPLING_KEY = "_sampling_priority_v1"; @@ -113,11 +111,6 @@ public boolean isError() { return error; } - @Override - public PayloadMessage.Type getMessageType() { - return PayloadMessage.Type.TRACE; - } - public static class TraceSpanContext implements Serializable { private static final long serialVersionUID = 1L; diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java new file mode 100644 index 000000000..790268f9d --- /dev/null +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java @@ -0,0 +1,96 @@ +package org.datadog.jenkins.plugins.datadog.traces.write; + +import hudson.model.Run; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.logging.Logger; +import net.sf.json.JSONObject; +import org.datadog.jenkins.plugins.datadog.clients.DatadogAgentClient; +import org.datadog.jenkins.plugins.datadog.model.BuildData; +import org.jenkinsci.plugins.workflow.graph.FlowNode; + +/** + * Trace write strategy that can dynamically switch from using APM track to using EVP Proxy. + * The switch will happen if an older agent (one that doesn't support EVP Proxy) is replaced with a newer agent. + */ +public class AgentTraceWriteStrategy implements TraceWriteStrategy { + + private static final Logger logger = Logger.getLogger(AgentTraceWriteStrategy.class.getName()); + + /** + * How often to check the /info endpoint in case the Agent got updated. + */ + private static final long EVP_PROXY_SUPPORT_TIME_BETWEEN_CHECKS_MS = TimeUnit.HOURS.toMillis(1); + + private final TraceWriteStrategy evpProxyStrategy; + private final TraceWriteStrategy apmStrategy; + private final Supplier checkEvpProxySupport; + private volatile boolean evpProxySupported = false; + private volatile long lastEvpProxyCheckTimeMs = 0L; + + public AgentTraceWriteStrategy(TraceWriteStrategy evpProxyStrategy, TraceWriteStrategy apmStrategy, Supplier checkEvpProxySupport) { + this.evpProxyStrategy = evpProxyStrategy; + this.apmStrategy = apmStrategy; + this.checkEvpProxySupport = checkEvpProxySupport; + } + + @Override + public JSONObject serialize(BuildData buildData, Run run) { + return getCurrentStrategy().serialize(buildData, run); + } + + @Override + public Collection serialize(FlowNode flowNode, Run run) { + return getCurrentStrategy().serialize(flowNode, run); + } + + @Override + public void send(List spans) { + // we have to check serialized spans to know where to send them, + // because the serialization strategy might've changed in between serialize() and send() + if (isWebhook(spans)) { + evpProxyStrategy.send(spans); + } else { + apmStrategy.send(spans); + } + } + + private boolean isWebhook(List spans) { + if (spans.isEmpty()) { + return false; + } + JSONObject span = spans.iterator().next(); + return span.get("level") != null; + } + + private TraceWriteStrategy getCurrentStrategy() { + if (isEvpProxySupported()) { + return evpProxyStrategy; + } else { + return apmStrategy; + } + } + + private boolean isEvpProxySupported() { + if (evpProxySupported) { + return true; // Once we have seen an Agent that supports EVP Proxy, we never check again. + } + if (System.currentTimeMillis() < (lastEvpProxyCheckTimeMs + EVP_PROXY_SUPPORT_TIME_BETWEEN_CHECKS_MS)) { + return evpProxySupported; // Wait at least 1 hour between checks, return the cached value + } + synchronized (DatadogAgentClient.class) { + if (!evpProxySupported) { + evpProxySupported = checkEvpProxySupport.get(); + lastEvpProxyCheckTimeMs = System.currentTimeMillis(); + if (evpProxySupported) { + logger.info("EVP Proxy is supported by the Agent. We will not check again until the next boot."); + } else { + logger.info("The Agent doesn't support EVP Proxy, falling back to APM for CI Visibility. Requires Agent v6.42+ or 7.42+."); + } + } + } + return evpProxySupported; + } +} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java new file mode 100644 index 000000000..c73a7f7a4 --- /dev/null +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java @@ -0,0 +1,16 @@ +package org.datadog.jenkins.plugins.datadog.traces.write; + +import hudson.model.Run; +import java.util.Collection; +import java.util.List; +import net.sf.json.JSONObject; +import org.datadog.jenkins.plugins.datadog.model.BuildData; +import org.jenkinsci.plugins.workflow.graph.FlowNode; + +public interface TraceWriteStrategy { + JSONObject serialize(BuildData buildData, Run run); + + Collection serialize(FlowNode flowNode, Run run); + + void send(List spans); +} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java new file mode 100644 index 000000000..ce6e822ae --- /dev/null +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java @@ -0,0 +1,56 @@ +package org.datadog.jenkins.plugins.datadog.traces.write; + +import hudson.model.Run; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Logger; +import net.sf.json.JSONObject; +import org.datadog.jenkins.plugins.datadog.DatadogUtilities; +import org.datadog.jenkins.plugins.datadog.model.BuildData; +import org.datadog.jenkins.plugins.datadog.traces.DatadogBaseBuildLogic; +import org.datadog.jenkins.plugins.datadog.traces.DatadogBasePipelineLogic; +import org.datadog.jenkins.plugins.datadog.util.CircuitBreaker; +import org.jenkinsci.plugins.workflow.graph.FlowNode; + +public class TraceWriteStrategyImpl implements TraceWriteStrategy { + + private static final Logger logger = Logger.getLogger(TraceWriteStrategyImpl.class.getName()); + + private final DatadogBaseBuildLogic buildLogic; + private final DatadogBasePipelineLogic pipelineLogic; + private final CircuitBreaker> sendSpansCircuitBreaker; + + public TraceWriteStrategyImpl(DatadogBaseBuildLogic buildLogic, DatadogBasePipelineLogic pipelineLogic, Consumer> spansSender) { + this.buildLogic = buildLogic; + this.pipelineLogic = pipelineLogic; + this.sendSpansCircuitBreaker = new CircuitBreaker<>( + spansSender, + this::logTransportBroken, + this::logTransportError + ); + } + + @Override + public JSONObject serialize(final BuildData buildData, final Run run) { + return buildLogic.finishBuildTrace(buildData, run); + } + + @Override + public Collection serialize(FlowNode flowNode, Run run) { + return pipelineLogic.execute(flowNode, run); + } + + @Override + public void send(List spans) { + sendSpansCircuitBreaker.accept(spans); + } + + private void logTransportBroken(List spans) { + logger.fine("Ignoring " + spans.size() + " because transport is broken"); + } + + private void logTransportError(Exception e) { + DatadogUtilities.severe(logger, e, "Error while sending trace"); + } +} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java new file mode 100644 index 000000000..fc532fe05 --- /dev/null +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java @@ -0,0 +1,118 @@ +package org.datadog.jenkins.plugins.datadog.traces.write; + +import hudson.model.Run; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; +import net.sf.json.JSONObject; +import org.datadog.jenkins.plugins.datadog.DatadogClient; +import org.datadog.jenkins.plugins.datadog.DatadogUtilities; +import org.datadog.jenkins.plugins.datadog.model.BuildData; +import org.jenkinsci.plugins.workflow.graph.FlowNode; + +public class TraceWriter { + + private static final Logger logger = Logger.getLogger(TraceWriter.class.getName()); + + private static final String QUEUE_CAPACITY_ENV_VAR = "DD_JENKINS_TRACES_QUEUE_CAPACITY"; + private static final String SUBMIT_TIMEOUT_ENV_VAR = "DD_JENKINS_TRACES_SUBMIT_TIMEOUT_SECONDS"; + private static final String STOP_TIMEOUT_ENV_VAR = "DD_JENKINS_TRACES_STOP_TIMEOUT_SECONDS"; + private static final String POLLING_INTERVAL_ENV_VAR = "DD_JENKINS_TRACES_POLLING_INTERVAL_SECONDS"; + private static final String BATCH_SIZE_LIMIT_ENV_VAR = "DD_JENKINS_TRACES_BATCH_SIZE_LIMIT"; + private static final int DEFAULT_QUEUE_CAPACITY = 10_000; + private static final int DEFAULT_SUBMIT_TIMEOUT_SECONDS = 30; + private static final int DEFAULT_STOP_TIMEOUT_SECONDS = 15; + private static final int DEFAULT_POLLING_INTERVAL_SECONDS = 10; + private static final int DEFAULT_BATCH_SIZE_LIMIT = 100; + + private final TraceWriteStrategy traceWriteStrategy; + private final BlockingQueue queue; + private final Thread poller; + + public TraceWriter(DatadogClient datadogClient) { + this.traceWriteStrategy = datadogClient.createTraceWriteStrategy(); + + this.queue = new ArrayBlockingQueue<>(getEnv(QUEUE_CAPACITY_ENV_VAR, DEFAULT_QUEUE_CAPACITY)); + + this.poller = new Thread(new QueuePoller(traceWriteStrategy, queue), "DD-Trace-Writer"); + this.poller.setDaemon(true); + this.poller.start(); + } + + public void submitBuild(final BuildData buildData, final Run run) throws InterruptedException, TimeoutException { + JSONObject buildJson = traceWriteStrategy.serialize(buildData, run); + submit(buildJson); + } + + public void submitPipeline(FlowNode flowNode, Run run) throws InterruptedException, TimeoutException { + Collection nodeJsons = traceWriteStrategy.serialize(flowNode, run); + for (JSONObject nodeJson : nodeJsons) { + submit(nodeJson); + } + } + + private void submit(JSONObject json) throws InterruptedException, TimeoutException { + if (!queue.offer(json, getEnv(SUBMIT_TIMEOUT_ENV_VAR, DEFAULT_SUBMIT_TIMEOUT_SECONDS), TimeUnit.SECONDS)) { + throw new TimeoutException("Timed out while submitting span"); + } + } + + public void stop() { + poller.interrupt(); + } + + private static final class QueuePoller implements Runnable { + private final TraceWriteStrategy traceWriteStrategy; + private final BlockingQueue queue; + + public QueuePoller(TraceWriteStrategy traceWriteStrategy, BlockingQueue queue) { + this.traceWriteStrategy = traceWriteStrategy; + this.queue = queue; + } + + @Override + public void run() { + long shutdownAt = Long.MAX_VALUE; + while (System.currentTimeMillis() < shutdownAt) { + try { + JSONObject span = queue.poll(getEnv(POLLING_INTERVAL_ENV_VAR, DEFAULT_POLLING_INTERVAL_SECONDS), TimeUnit.SECONDS); + if (span == null) { + continue; // nothing to send + } + + int batchSize = getEnv(BATCH_SIZE_LIMIT_ENV_VAR, DEFAULT_BATCH_SIZE_LIMIT); + List spans = new ArrayList<>(batchSize); + spans.add(span); + queue.drainTo(spans, batchSize - 1); + + traceWriteStrategy.send(spans); + + } catch (InterruptedException e) { + logger.info("Queue poller thread interrupted"); + shutdownAt = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(getEnv(STOP_TIMEOUT_ENV_VAR, DEFAULT_STOP_TIMEOUT_SECONDS)); + + } catch (Exception e) { + DatadogUtilities.severe(logger, e, "Error while sending trace"); + } + } + logger.info("Queue poller thread shut down"); + } + } + + private static int getEnv(String envVar, int defaultValue) { + String value = System.getenv(envVar); + if (value != null) { + try { + return Integer.parseInt(value); + } catch (Exception e) { + DatadogUtilities.severe(logger, null, "Invalid value " + value + " provided for env var " + envVar + ": integer number expected"); + } + } + return defaultValue; + } +} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java new file mode 100644 index 000000000..d8691f3dd --- /dev/null +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java @@ -0,0 +1,30 @@ +package org.datadog.jenkins.plugins.datadog.traces.write; + +import javax.annotation.Nullable; +import org.datadog.jenkins.plugins.datadog.DatadogClient; +import org.datadog.jenkins.plugins.datadog.clients.ClientFactory; + +public class TraceWriterFactory { + + private static volatile TraceWriter TRACE_WRITER; + + public static synchronized void onDatadogClientUpdate(@Nullable DatadogClient client) { + if (client == null) { + return; + } + + if (TRACE_WRITER != null) { + TRACE_WRITER.stop(); + } + + TRACE_WRITER = new TraceWriter(client); + } + + @Nullable + public static TraceWriter getTraceWriter() { + if (TRACE_WRITER == null) { + onDatadogClientUpdate(ClientFactory.getClient()); + } + return TRACE_WRITER; + } +} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpClient.java deleted file mode 100644 index 0e2802334..000000000 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpClient.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import java.util.List; - -public interface HttpClient { - - void send(List messages); - - void stop(); - - void close(); -} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpErrorHandler.java b/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpErrorHandler.java deleted file mode 100644 index 3231e2ba8..000000000 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpErrorHandler.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -public interface HttpErrorHandler { - - void handle(Exception exception); -} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpMessage.java b/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpMessage.java deleted file mode 100644 index 886cd713b..000000000 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpMessage.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import org.datadog.jenkins.plugins.datadog.util.SuppressFBWarnings; - -import java.net.URL; - -@SuppressFBWarnings -public class HttpMessage { - - private final URL url; - private final HttpMethod method; - private final String contentType; - private final byte[] payload; - - public HttpMessage(URL url, HttpMethod method, String contentType, byte[] payload) { - this.url = url; - this.method = method; - this.contentType = contentType; - this.payload = payload; - } - - public URL getURL() { - return this.url; - } - - public HttpMethod getMethod() { - return this.method; - } - - public String getContentType() { - return this.contentType; - } - - public byte[] getPayload() { - return this.payload; - } - - @Override - public String toString() { - return "HttpMessage{" + - "url=" + url + - ", method=" + method + - ", contentType='" + contentType + '\'' + - '}'; - } - - public enum HttpMethod { - PUT - } -} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpMessageFactory.java b/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpMessageFactory.java deleted file mode 100644 index f1126d2a8..000000000 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpMessageFactory.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import java.net.URL; -import java.util.List; - -public class HttpMessageFactory { - - private final URL url; - private final HttpMessage.HttpMethod httpMethod; - private final PayloadMapper> payloadMapper; - - private HttpMessageFactory(final Builder builder) { - this.url = builder.agentURL; - this.httpMethod = builder.httpMethod; - this.payloadMapper = builder.payloadMapper; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private URL agentURL; - private HttpMessage.HttpMethod httpMethod; - private PayloadMapper> payloadMapper; - - public Builder agentURL(URL agentURL) { - this.agentURL = agentURL; - return this; - } - - public Builder httpMethod(final HttpMessage.HttpMethod httpMethod) { - this.httpMethod = httpMethod; - return this; - } - - public Builder payloadMapper(final PayloadMapper payloadMapper) { - this.payloadMapper = payloadMapper; - return this; - } - - public HttpMessageFactory build() { - return new HttpMessageFactory(this); - } - } - - public URL getURL() { - return this.url; - } - - public HttpMessage create(List messages) { - return new HttpMessage(this.url, this.httpMethod, this.payloadMapper.contentType(), this.payloadMapper.map(messages)); - } -} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpSender.java b/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpSender.java deleted file mode 100644 index 6d0a0da13..000000000 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/HttpSender.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; -import org.datadog.jenkins.plugins.datadog.DatadogUtilities; -import org.datadog.jenkins.plugins.datadog.clients.HttpClient; - -public class HttpSender implements Runnable { - - private static final Logger logger = Logger.getLogger(HttpSender.class.getName()); - - private final BlockingQueue queue; - private final HttpErrorHandler errorHandler; - private final HttpClient client; - - private volatile boolean shutdown; - - HttpSender(final int queueSize, final HttpErrorHandler errorHandler, final int httpTimeoutMs) { - this(new ArrayBlockingQueue<>(queueSize), errorHandler, httpTimeoutMs); - } - - HttpSender(final BlockingQueue queue, final HttpErrorHandler errorHandler, final int httpTimeoutMs) { - this.queue = queue; - this.errorHandler = errorHandler; - this.client = new HttpClient(httpTimeoutMs); - } - - boolean send(final HttpMessage message){ - if (shutdown) { - return false; - } - try { - queue.put(message); - return true; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return false; - } - } - - @Override - public void run() { - // Consumer loop. - // Consume till shutdown=true and queue is empty. - while (!queue.isEmpty() || !shutdown) { - try { - // Try to retrieve (and remove) the head of the queue - // with 1 second of timeout to avoid blocking - // the thread indefinitely. - final HttpMessage message = queue.poll(1, TimeUnit.SECONDS); - if (null != message) { - process(message); - } - } catch (final InterruptedException e) { - if (shutdown) { - return; - } - } catch (final Exception e) { - errorHandler.handle(e); - } - } - } - - protected void process(HttpMessage message) { - try { - client.sendAsynchronously(message); - } catch (Exception e) { - DatadogUtilities.severe(logger, e, "Error while sending message: " + message); - } - } - - void shutdown() { - shutdown = true; - } -} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/LoggerHttpErrorHandler.java b/src/main/java/org/datadog/jenkins/plugins/datadog/transport/LoggerHttpErrorHandler.java deleted file mode 100644 index 8e32db7e3..000000000 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/LoggerHttpErrorHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import org.datadog.jenkins.plugins.datadog.DatadogUtilities; - -import java.util.logging.Logger; - -public class LoggerHttpErrorHandler implements HttpErrorHandler{ - - public static final HttpErrorHandler LOGGER_HTTP_ERROR_HANDLER = new LoggerHttpErrorHandler(); - - private static final Logger logger = Logger.getLogger(LoggerHttpErrorHandler.class.getName()); - - @Override - public void handle(Exception exception) { - DatadogUtilities.severe(logger, exception, exception.getMessage()); - } -} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/NonBlockingHttpClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/transport/NonBlockingHttpClient.java deleted file mode 100644 index 960c44534..000000000 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/NonBlockingHttpClient.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - -public class NonBlockingHttpClient implements HttpClient { - - private static final int DEFAULT_TIMEOUT_MS = 10 * 1000; - private static final int DEFAULT_MAX_QUEUE_SIZE = 10_000; - private static final int SIZE_SPANS_SEND_BUFFER = 100; - - private static final Logger logger = Logger.getLogger(NonBlockingHttpClient.class.getName()); - - private static final HttpErrorHandler NO_OP_HANDLER = new HttpErrorHandler() { - @Override public void handle(final Exception e) { /* No-op */ } - }; - - private final HttpErrorHandler errorHandler; - private final HttpSender sender; - private final Map messageFactoryByType; - - private final ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() { - final ThreadFactory delegate = Executors.defaultThreadFactory(); - @Override public Thread newThread(final Runnable r) { - final Thread result = delegate.newThread(r); - result.setName("DDNonBlockingHttpClient-" + result.getName()); - result.setDaemon(true); - return result; - } - }); - - private NonBlockingHttpClient(final Builder builder) { - final int queueSize = builder.queueSize != null ? builder.queueSize : DEFAULT_MAX_QUEUE_SIZE; - final int httpTimeoutMs = builder.httpTimeoutMs != null ? builder.httpTimeoutMs : DEFAULT_TIMEOUT_MS; - this.errorHandler = builder.errorHandler != null ? builder.errorHandler : NO_OP_HANDLER; - this.messageFactoryByType = builder.messageFactoryByType; - this.sender = createSender(queueSize, errorHandler, httpTimeoutMs); - executor.submit(sender); - - if(this.messageFactoryByType != null) { - for(Map.Entry messageFactoryEntry : messageFactoryByType.entrySet()) { - logger.info(messageFactoryEntry.getKey() + " -> " + messageFactoryEntry.getValue().getURL()); - } - } - } - - public static Builder builder() { - return new Builder(); - } - - private HttpSender createSender(final int queueSize, final HttpErrorHandler errorHandler, final int httpTimeoutMs) { - return new HttpSender(queueSize, errorHandler, httpTimeoutMs); - } - - public void send(List messages) { - if(messages != null && !messages.isEmpty()) { - final List spanSendBuffer = new ArrayList<>(SIZE_SPANS_SEND_BUFFER); - for(int i = 0; i < messages.size(); i++) { - spanSendBuffer.add(messages.get(i)); - - // Send every 100 spans or the last one. - if(spanSendBuffer.size() == SIZE_SPANS_SEND_BUFFER || i == (messages.size() - 1)) { - final List buffer = Collections.unmodifiableList(spanSendBuffer); - // We assume all payload messages belong to the same message type for now. - final PayloadMessage.Type type = buffer.get(0).getMessageType(); - final HttpMessage message = this.messageFactoryByType.get(type).create(buffer); - this.sender.send(message); - spanSendBuffer.clear(); - } - } - } - } - - @Override - public void stop() { - try { - sender.shutdown(); - executor.shutdown(); - try { - executor.awaitTermination(30, TimeUnit.SECONDS); - if (!executor.isTerminated()) { - executor.shutdownNow(); - } - } catch (Exception e) { - errorHandler.handle(e); - if (!executor.isTerminated()) { - executor.shutdownNow(); - } - } - } catch (final Exception e) { - errorHandler.handle(e); - } - } - - @Override - public void close() { - stop(); - } - - public static class Builder { - - private HttpErrorHandler errorHandler; - private Integer queueSize; - private Integer httpTimeoutMs; - private Map messageFactoryByType = new HashMap<>(); - - public Builder errorHandler(final HttpErrorHandler errorHandler) { - this.errorHandler = errorHandler; - return this; - } - - public Builder queueSize(final int queueSize) { - this.queueSize = queueSize; - return this; - } - - public Builder messageRoute(final PayloadMessage.Type key, final HttpMessageFactory messageFactory) { - this.messageFactoryByType.put(key, messageFactory); - return this; - } - - public Builder httpTimeoutMs(final int httpTimeoutMs) { - this.httpTimeoutMs = httpTimeoutMs; - return this; - } - - public NonBlockingHttpClient build() { - return new NonBlockingHttpClient(this); - } - - } -} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/PayloadMapper.java b/src/main/java/org/datadog/jenkins/plugins/datadog/transport/PayloadMapper.java deleted file mode 100644 index 18c9f1285..000000000 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/PayloadMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import java.util.List; - -public interface PayloadMapper> { - - byte[] map(final T list); - - String contentType(); -} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/PayloadMessage.java b/src/main/java/org/datadog/jenkins/plugins/datadog/transport/PayloadMessage.java deleted file mode 100644 index 3d0aa556a..000000000 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/transport/PayloadMessage.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -public interface PayloadMessage { - - Type getMessageType(); - - enum Type { - TRACE - } -} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/util/CircuitBreaker.java b/src/main/java/org/datadog/jenkins/plugins/datadog/util/CircuitBreaker.java new file mode 100644 index 000000000..0cd5d5126 --- /dev/null +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/util/CircuitBreaker.java @@ -0,0 +1,83 @@ +package org.datadog.jenkins.plugins.datadog.util; + +import java.util.function.Consumer; +import javax.annotation.concurrent.GuardedBy; + +public class CircuitBreaker { + + private static final int DEFAULT_MIN_HEALTH_CHECK_DELAY_MILLIS = 1000; + private static final int DEFAULT_MAX_HEALTH_CHECK_DELAY_MILLIS = 60000; + private static final double DEFAULT_DELAY_FACTOR = 2.0; + + private final Consumer action; + private final Consumer fallback; + private final Consumer errorHandler; + private final long minHealthCheckDelayMillis; + private final long maxHealthCheckDelayMillis; + private final double delayFactor; + + @GuardedBy("this") + private boolean healthy; + @GuardedBy("this") + private long healthCheckDelayMillis; + @GuardedBy("this") + private long healthCheckAt; + + public CircuitBreaker(Consumer action, Consumer fallback, Consumer errorHandler) { + this(action, fallback, errorHandler, DEFAULT_MIN_HEALTH_CHECK_DELAY_MILLIS, DEFAULT_MAX_HEALTH_CHECK_DELAY_MILLIS, DEFAULT_DELAY_FACTOR); + } + + public CircuitBreaker(Consumer action, + Consumer fallback, + Consumer errorHandler, + long minHealthCheckDelayMillis, + long maxHealthCheckDelayMillis, + double delayFactor) { + this.action = action; + this.fallback = fallback; + this.errorHandler = errorHandler; + this.minHealthCheckDelayMillis = minHealthCheckDelayMillis; + this.maxHealthCheckDelayMillis = maxHealthCheckDelayMillis; + this.delayFactor = delayFactor; + synchronized (this) { + this.healthy = true; + this.healthCheckDelayMillis = minHealthCheckDelayMillis; + } + } + + public synchronized void accept(T t) { + // normal flow + if (healthy) { + try { + action.accept(t); + } catch (Exception e) { + errorHandler.accept(e); + healthy = false; + healthCheckAt = System.currentTimeMillis() + healthCheckDelayMillis; + fallback.accept(t); + } + + // try to recover + } else if (System.currentTimeMillis() >= healthCheckAt) { + try { + action.accept(t); + healthy = true; + healthCheckDelayMillis = minHealthCheckDelayMillis; + } catch (Exception e) { + errorHandler.accept(e); + healthCheckDelayMillis = Math.min(Math.round(healthCheckDelayMillis * delayFactor), maxHealthCheckDelayMillis); + healthCheckAt = System.currentTimeMillis() + healthCheckDelayMillis; + fallback.accept(t); + } + + // "broken" flow + } else { + fallback.accept(t); + } + } + + public synchronized void reset() { + healthy = true; + healthCheckDelayMillis = minHealthCheckDelayMillis; + } +} diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java b/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java index c3bb6067b..098351110 100644 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java +++ b/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java @@ -26,27 +26,29 @@ of this software and associated documentation files (the "Software"), to deal package org.datadog.jenkins.plugins.datadog.clients; import hudson.model.Run; -import hudson.util.Secret; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import net.sf.json.JSONObject; import org.datadog.jenkins.plugins.datadog.DatadogClient; import org.datadog.jenkins.plugins.datadog.DatadogEvent; import org.datadog.jenkins.plugins.datadog.model.BuildData; -import org.datadog.jenkins.plugins.datadog.traces.DatadogBaseBuildLogic; -import org.datadog.jenkins.plugins.datadog.traces.DatadogBasePipelineLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogTraceBuildLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogTracePipelineLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookBuildLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookPipelineLogic; -import org.datadog.jenkins.plugins.datadog.transport.FakeTracesHttpClient; +import org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper; +import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; +import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.junit.Assert; @@ -56,91 +58,12 @@ public class DatadogClientStub implements DatadogClient { public List serviceChecks; public List events; public List logLines; - public List webhooks; - - private List webhookLatches; - - public FakeTracesHttpClient agentHttpClient; - - public DatadogBaseBuildLogic traceBuildLogic; - public DatadogBasePipelineLogic tracePipelineLogic; - public DatadogClientStub() { this.metrics = new ArrayList<>(); this.serviceChecks = new ArrayList<>(); this.events = new ArrayList<>(); this.logLines = new ArrayList<>(); - this.webhooks = new ArrayList<>(); - this.webhookLatches = new ArrayList<>(); - this.agentHttpClient = new FakeTracesHttpClient(); - this.traceBuildLogic = new DatadogTraceBuildLogic(this.agentHttpClient); - this.tracePipelineLogic = new DatadogTracePipelineLogic(this.agentHttpClient); - } - - @Override - public void setUrl(String url) { - // noop - } - - @Override - public void setLogIntakeUrl(String logIntakeUrl) { - // noop - } - - @Override - public void setWebhookIntakeUrl(String webhookIntakeUrl) { - // noop - } - - @Override - public void setApiKey(Secret apiKey) { - // noop - } - - @Override - public void setHostname(String hostname) { - // noop - } - - @Override - public void setPort(Integer port) { - // noop - } - - @Override - public void setLogCollectionPort(Integer logCollectionPort) { - - } - - @Override - public boolean isDefaultIntakeConnectionBroken() { - return false; - } - - @Override - public void setDefaultIntakeConnectionBroken(boolean defaultIntakeConnectionBroken) { - // noop - } - - @Override - public boolean isLogIntakeConnectionBroken() { - return false; - } - - @Override - public void setLogIntakeConnectionBroken(boolean logIntakeConnectionBroken) { - // noop - } - - @Override - public boolean isWebhookIntakeConnectionBroken() { - return false; - } - - @Override - public void setWebhookIntakeConnectionBroken(boolean webhookIntakeConnectionBroken) { - // noop } @Override @@ -198,44 +121,6 @@ public boolean sendLogs(String payloadLogs) { return true; } - @Override - public boolean postWebhook(String webhook) { - synchronized (webhookLatches) { - JSONObject payload = JSONObject.fromObject(webhook); - webhooks.add(payload); - for(final CountDownLatch latch : webhookLatches) { - if(webhooks.size() >= latch.getCount()) { - while (latch.getCount() > 0) { - latch.countDown(); - } - } - } - } - return true; - } - - @Override - public boolean startBuildTrace(BuildData buildData, Run run) { - this.traceBuildLogic.startBuildTrace(buildData, run); - return true; - } - - @Override - public boolean finishBuildTrace(BuildData buildData, Run run) { - this.traceBuildLogic.finishBuildTrace(buildData, run); - return true; - } - - @Override - public boolean sendPipelineTrace(Run run, FlowNode flowNode) { - this.tracePipelineLogic.execute(run, flowNode); - return true; - } - - public FakeTracesHttpClient agentHttpClient(){ - return this.agentHttpClient; - } - public boolean assertMetric(String name, double value, String hostname, String[] tags) { DatadogMetric m = new DatadogMetric(name, value, hostname, Arrays.asList(tags)); if (this.metrics.contains(m)) { @@ -372,24 +257,103 @@ public static Map> addTagToMap(Map> tags return tags; } + private final StubTraceWriteStrategy traceWriteStrategy = new StubTraceWriteStrategy(); + + private static final class StubTraceWriteStrategy implements TraceWriteStrategy { + private volatile boolean isWebhook = false; + private final Collection traces = new LinkedBlockingQueue<>(); + private final Collection webhooks = new LinkedBlockingQueue<>(); + + @Override + public JSONObject serialize(BuildData buildData, Run run) { + if (isWebhook) { + JSONObject json = new DatadogWebhookBuildLogic().finishBuildTrace(buildData, run); + webhooks.add(json); + return json; + } else { + TraceSpan span = new DatadogTraceBuildLogic().createSpan(buildData, run); + traces.add(span); + return new JsonTraceSpanMapper().map(span); + } + } + + @Override + public Collection serialize(FlowNode flowNode, Run run) { + if (isWebhook) { + Collection spans = new DatadogWebhookPipelineLogic().execute(flowNode, run); + webhooks.addAll(spans); + return spans; + } else { + Collection traceSpans = new DatadogTracePipelineLogic().collectTraces(flowNode, run); + traces.addAll(traceSpans); + JsonTraceSpanMapper mapper = new JsonTraceSpanMapper(); + return traceSpans.stream().map(mapper::map).collect(Collectors.toList()); + } + } + + @Override + public void send(List spans) { + // no op + } + + public void configureForWebhooks() { + isWebhook = true; + } + } + public void configureForWebhooks() { - traceBuildLogic = new DatadogWebhookBuildLogic(this); - tracePipelineLogic = new DatadogWebhookPipelineLogic(this); + traceWriteStrategy.configureForWebhooks(); + } + + @Override + public TraceWriteStrategy createTraceWriteStrategy() { + return traceWriteStrategy; } public boolean waitForWebhooks(final int number) throws InterruptedException { - final CountDownLatch latch = new CountDownLatch(number); - synchronized (webhookLatches) { - if (webhooks.size() >= number) { + long timeout = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10); + while (System.currentTimeMillis() < timeout) { + if (traceWriteStrategy.webhooks.size() >= number) { return true; } - webhookLatches.add(latch); + Thread.sleep(100L); + } + if (traceWriteStrategy.webhooks.size() < number) { + throw new AssertionError("Failed while waiting for " + number + " webhooks, got " + traceWriteStrategy.webhooks.size() + ": " + traceWriteStrategy.webhooks); + } else { + return true; + } + } + + public boolean waitForTraces(int number) throws InterruptedException { + long timeout = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10); + while (System.currentTimeMillis() < timeout) { + if (traceWriteStrategy.traces.size() >= number) { + return true; + } + Thread.sleep(100L); + } + if (traceWriteStrategy.traces.size() < number) { + throw new AssertionError("Failed while waiting for " + number + " traces, got " + traceWriteStrategy.traces.size() + ": " + traceWriteStrategy.traces); + } else { + return true; } - return latch.await(10, TimeUnit.SECONDS); } public List getWebhooks() { - return this.webhooks; + return new ArrayList<>(traceWriteStrategy.webhooks); } + public List getSpans() { + ArrayList spans = new ArrayList<>(traceWriteStrategy.traces); + Collections.sort(spans, (span1, span2) -> { + if(span1.getStartNano() < span2.getStartNano()){ + return -1; + } else if (span1.getStartNano() > span2.getStartNano()) { + return 1; + } + return 0; + }); + return spans; + } } diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientTest.java b/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientTest.java index 1d44c7b43..f121a2aaa 100644 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientTest.java +++ b/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientTest.java @@ -51,8 +51,8 @@ public class DatadogClientTest { public void testHttpClientGetInstanceApiKey() { //validateConfiguration throws an error when given an invalid API key when the urls are valid Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> { - DatadogHttpClient.enableValidations = false; - DatadogHttpClient client = (DatadogHttpClient) DatadogHttpClient.getInstance("http", "test", "test", null); + DatadogApiClient.enableValidations = false; + DatadogApiClient client = (DatadogApiClient) DatadogApiClient.getInstance("http", "test", "test", null); client.validateConfiguration(); }); @@ -65,8 +65,8 @@ public void testHttpClientGetInstanceApiKey() { public void testHttpClientGetInstanceApiUrl() { // validateConfiguration throws an error when given an invalid url Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> { - DatadogHttpClient.enableValidations = false; - DatadogHttpClient client = (DatadogHttpClient) DatadogHttpClient.getInstance("", null, null, null); + DatadogApiClient.enableValidations = false; + DatadogApiClient client = (DatadogApiClient) DatadogApiClient.getInstance("", null, null, null); client.validateConfiguration(); }); String expectedMessage = "Datadog Target URL is not set properly"; @@ -78,8 +78,8 @@ public void testHttpClientGetInstanceApiUrl() { @Test public void testHttpClientGetInstanceEnableValidations() { // calling getInstance with invalid data returns null - DatadogHttpClient.enableValidations = true; - DatadogClient client = DatadogHttpClient.getInstance("https", null, null, null); + DatadogApiClient.enableValidations = true; + DatadogClient client = DatadogApiClient.getInstance("https", null, null, null); Assert.assertEquals(client, null); } @@ -112,7 +112,7 @@ public void testEvpProxyEnabled() { cfg.setEnableCiVisibility(true); DatadogAgentClient client = Mockito.spy(new DatadogAgentClient("test",1234, 1235, 1236)); Mockito.doReturn(new HashSet(Arrays.asList("/evp_proxy/v3/"))).when(client).fetchAgentSupportedEndpoints(); - Assert.assertTrue(client.checkEvpProxySupportAndUpdateLogic()); + Assert.assertTrue(client.isEvpProxySupported()); } @Test @@ -121,7 +121,7 @@ public void testEvpProxyDisabled() { cfg.setEnableCiVisibility(true); DatadogAgentClient client = Mockito.spy(new DatadogAgentClient("test",1234, 1235, 1236)); Mockito.doReturn(new HashSet()).when(client).fetchAgentSupportedEndpoints(); - Assert.assertFalse(client.checkEvpProxySupportAndUpdateLogic()); + Assert.assertFalse(client.isEvpProxySupported()); } @Test @@ -134,8 +134,8 @@ public void testEmptyAgentSupportedEndpointsWithNoAgent() { @Test public void testIncrementCountAndFlush() throws IOException, InterruptedException { - DatadogHttpClient.enableValidations = false; - DatadogClient client = DatadogHttpClient.getInstance("test", null, null, null); + DatadogApiClient.enableValidations = false; + DatadogClient client = DatadogApiClient.getInstance("test", null, null, null); Map> tags1 = new HashMap<>(); tags1 = DatadogClientStub.addTagToMap(tags1, "tag1", "value"); tags1 = DatadogClientStub.addTagToMap(tags1, "tag2", "value"); @@ -196,8 +196,8 @@ public void testIncrementCountAndFlushThreadedEnv() throws IOException, Interrup @Override public void run() { // We use a new instance of a client on every run. - DatadogHttpClient.enableValidations = false; - DatadogClient client = DatadogHttpClient.getInstance("test2", null, null, null); + DatadogApiClient.enableValidations = false; + DatadogClient client = DatadogApiClient.getInstance("test2", null, null, null); Map> tags = new HashMap<>(); tags = DatadogClientStub.addTagToMap(tags, "tag1", "value"); tags = DatadogClientStub.addTagToMap(tags, "tag2", "value"); @@ -225,8 +225,8 @@ public void testIncrementCountAndFlushThreadedEnvThreadCheck() throws IOExceptio @Override public void run() { // We use a new instance of a client on every run. - DatadogHttpClient.enableValidations = false; - DatadogClient client = DatadogHttpClient.getInstance("test3", null, null, null); + DatadogApiClient.enableValidations = false; + DatadogClient client = DatadogApiClient.getInstance("test3", null, null, null); Map> tags = new HashMap<>(); tags = DatadogClientStub.addTagToMap(tags, "tag1", "value"); tags = DatadogClientStub.addTagToMap(tags, "tag2", "value"); @@ -264,8 +264,8 @@ public Boolean call() throws Exception { @Test public void testIncrementCountAndFlushThreadedEnvOneClient() throws IOException, InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(2); - DatadogHttpClient.enableValidations = false; - final DatadogClient client = DatadogHttpClient.getInstance("testing", null, null, null); + DatadogApiClient.enableValidations = false; + final DatadogClient client = DatadogApiClient.getInstance("testing", null, null, null); Runnable increment = new Runnable() { @Override public void run() { diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListenerIT.java b/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListenerIT.java index cbf1c0c56..3c159bb75 100644 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListenerIT.java +++ b/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListenerIT.java @@ -27,6 +27,12 @@ import hudson.model.Label; import hudson.slaves.DumbSlave; import hudson.slaves.EnvironmentVariablesNodeProperty; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; import jenkins.model.Jenkins; import net.sf.json.JSONArray; import net.sf.json.JSONObject; @@ -37,20 +43,12 @@ import org.datadog.jenkins.plugins.datadog.model.BuildPipelineNode; import org.datadog.jenkins.plugins.datadog.traces.CITags; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; -import org.datadog.jenkins.plugins.datadog.transport.FakeTracesHttpClient; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.jvnet.hudson.test.ExtractResourceSCM; import org.jvnet.hudson.test.JenkinsRule; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - public class DatadogBuildListenerIT extends DatadogTraceAbstractTest { private static final String SAMPLE_SERVICE_NAME = "sampleServiceName"; @@ -91,9 +89,8 @@ public void testTracesQueueTime() throws Exception{ DumbSlave worker = null; try { worker = jenkinsRule.createOnlineSlave(Label.get("testBuild")); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -129,9 +126,8 @@ public void testTraces() throws Exception { FreeStyleBuild run = project.scheduleBuild2(0).get(); final String buildPrefix = BuildPipelineNode.NodeType.PIPELINE.getTagName(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -185,9 +181,8 @@ public void testGitDefaultBranch() throws Exception { } project.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -217,9 +212,8 @@ public void testUserSuppliedGitWithoutCommitInfo() throws Exception { } project.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -255,9 +249,8 @@ public void testUserSuppliedGitWithCommitInfo() throws Exception { } project.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -278,8 +271,6 @@ public void testUserSuppliedGitWithCommitInfo() throws Exception { @Test public void testUserSuppliedGitWithCommitInfoWebhook() throws Exception { - clientStub.configureForWebhooks(); - Jenkins jenkins = jenkinsRule.jenkins; final EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); EnvVars env = prop.getEnvVars(); @@ -340,9 +331,8 @@ public void testRawRepositoryUrl() throws Exception { project.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -368,9 +358,8 @@ public void testFilterSensitiveInfoRepoUrl() throws Exception { project.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -384,8 +373,6 @@ public void testFilterSensitiveInfoRepoUrl() throws Exception { @Test public void testGitAlternativeRepoUrlWebhook() throws Exception { - clientStub.configureForWebhooks(); - Jenkins jenkins = jenkinsRule.jenkins; final EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); EnvVars env = prop.getEnvVars(); @@ -421,16 +408,13 @@ public void testTracesDisabled() throws Exception { final FreeStyleProject project = jenkinsRule.createFreeStyleProject("buildIntegrationSuccess-notraces"); project.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(0); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(0); + final List spans = clientStub.getSpans(); assertEquals(0, spans.size()); } @Test public void testTracesDisabledWebhooks() throws Exception { - clientStub.configureForWebhooks(); - DatadogGlobalConfiguration cfg = DatadogUtilities.getDatadogGlobalDescriptor(); cfg.setEnableCiVisibility(false); @@ -451,9 +435,8 @@ public void testCITagsOnTraces() throws Exception { final FreeStyleProject project = jenkinsRule.createFreeStyleProject("buildIntegrationSuccessTags_job"); project.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -464,8 +447,6 @@ public void testCITagsOnTraces() throws Exception { @Test public void testCITagsOnWebhooks() throws Exception { - clientStub.configureForWebhooks(); - DatadogGlobalConfiguration cfg = DatadogUtilities.getDatadogGlobalDescriptor(); cfg.setGlobalJobTags("(.*?)_job, global_job_tag:$ENV_VAR"); cfg.setGlobalTags("global_tag:$ENV_VAR"); @@ -489,9 +470,8 @@ public void testAvoidSettingEmptyGitInfoOnTraces() throws Exception { final FreeStyleProject project = jenkinsRule.createFreeStyleProject("buildIntegrationSuccessTagsNoGitInfo"); project.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListenerTest.java b/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListenerTest.java index a224dd3c8..12c3f19af 100644 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListenerTest.java +++ b/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListenerTest.java @@ -51,7 +51,6 @@ import org.datadog.jenkins.plugins.datadog.model.BuildPipelineNode; import org.datadog.jenkins.plugins.datadog.traces.CITags; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; -import org.datadog.jenkins.plugins.datadog.transport.FakeTracesHttpClient; import org.jenkinsci.plugins.workflow.actions.LabelAction; import org.jenkinsci.plugins.workflow.actions.ThreadNameAction; import org.jenkinsci.plugins.workflow.actions.TimingAction; @@ -192,9 +191,8 @@ public void testIntegration() throws Exception { } } - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(16); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(16); + final List spans = clientStub.getSpans(); assertEquals(16, spans.size()); } @@ -224,9 +222,8 @@ public void testIntegrationGitInfo() throws Exception { jenkins.getGlobalNodeProperties().add(prop); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); assertGitVariablesOnSpan(buildSpan, "master"); @@ -317,9 +314,8 @@ public void testIntegrationGitInfoDefaultBranchEnvVar() throws Exception { jenkins.getGlobalNodeProperties().add(prop); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); assertGitVariablesOnSpan(buildSpan, "hardcoded-master"); @@ -351,9 +347,8 @@ public void testIntegrationGitInfoOverrideCommit() throws Exception { jenkins.getGlobalNodeProperties().add(prop); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(5); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(5); + final List spans = clientStub.getSpans(); assertEquals(5, spans.size()); for(TraceSpan span : spans) { assertEquals("401d997a6eede777602669ccaec059755c98161f", span.getMeta().get(CITags.GIT_COMMIT_SHA)); @@ -386,9 +381,8 @@ public void testIntegrationGitAlternativeRepoUrl() throws Exception { jenkins.getGlobalNodeProperties().add(prop); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(5); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(5); + final List spans = clientStub.getSpans(); assertEquals(5, spans.size()); for(TraceSpan span : spans) { assertEquals("https://github.com/johndoe/foobar.git", span.getMeta().get(CITags.GIT_REPOSITORY_URL)); @@ -458,9 +452,8 @@ public void testUserSuppliedGitWithoutCommitInfo() throws Exception { jenkins.getGlobalNodeProperties().add(prop); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); assertGitVariablesOnSpan(buildSpan, "master"); @@ -539,9 +532,8 @@ public void testUserSuppliedGitWithCommitInfo() throws Exception { jenkins.getGlobalNodeProperties().add(prop); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); final Map meta = buildSpan.getMeta(); @@ -577,9 +569,8 @@ public void testRawRepositoryUrl() throws Exception { jenkins.getGlobalNodeProperties().add(prop); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); final Map meta = buildSpan.getMeta(); @@ -607,9 +598,8 @@ public void testFilterSensitiveInfoRepoUrl() throws Exception { jenkins.getGlobalNodeProperties().add(prop); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); final Map meta = buildSpan.getMeta(); @@ -636,9 +626,8 @@ public void testStageNamePropagation() throws Exception{ }).start(); jenkinsRule.createOnlineSlave(Label.get("testStageName")); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(6); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(6); + final List spans = clientStub.getSpans(); assertEquals(6, spans.size()); final TraceSpan stage1 = searchSpan(spans, "Stage 1"); @@ -720,9 +709,8 @@ public void testIntegrationPipelineQueueTimeOnStages() throws Exception { Thread.sleep(10000); final DumbSlave worker = jenkinsRule.createOnlineSlave(Label.get("testStage")); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(6); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(6); + final List spans = clientStub.getSpans(); assertEquals(6, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -830,9 +818,8 @@ public void testIntegrationPipelineQueueTimeOnPipeline() throws Exception { Thread.sleep(15000); final DumbSlave worker = jenkinsRule.createOnlineSlave(Label.get("testPipeline")); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -884,9 +871,8 @@ public void testIntegrationNoFailureTag() throws Exception { final String stagePrefix = BuildPipelineNode.NodeType.STAGE.getTagName(); final String stepPrefix = BuildPipelineNode.NodeType.STEP.getTagName(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -975,9 +961,8 @@ public void testIntegrationPipelineSkippedLogic() throws Exception { job.setDefinition(new CpsFlowDefinition(definition, true)); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(2); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(2); + final List spans = clientStub.getSpans(); assertEquals(2, spans.size()); final TraceSpan stage = spans.get(1); @@ -1032,9 +1017,8 @@ public void testIntegrationTracesDisabled() throws Exception{ clientStub.assertMetric("jenkins.job.stage_duration", hostname, tags); clientStub.assertMetric("jenkins.job.stage_pause_duration", 0, hostname, tags); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(0); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(0); + final List spans = clientStub.getSpans(); assertEquals(0, spans.size()); } @@ -1094,9 +1078,8 @@ public void testStagesNodeNames_complexPipelineStages01() throws Exception { final DumbSlave worker02 = jenkinsRule.createOnlineSlave(Label.get("worker02")); final DumbSlave worker03 = jenkinsRule.createOnlineSlave(Label.get("worker03")); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(19); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(19); + final List spans = clientStub.getSpans(); assertEquals(19, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -1190,9 +1173,8 @@ public void testGlobalTagsPropagationsTraces() throws Exception { job.setDefinition(new CpsFlowDefinition(definition, true)); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -1227,9 +1209,8 @@ public void testErrorPropagationOnStages() throws Exception { job.setDefinition(new CpsFlowDefinition(definition, true)); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan stepSpan = spans.get(2); @@ -1294,9 +1275,8 @@ public void testUnstablePropagationOnStages() throws Exception { job.setDefinition(new CpsFlowDefinition(definition, true)); job.scheduleBuild2(0).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan stepSpan = spans.get(2); @@ -1365,9 +1345,8 @@ public void testCustomHostnameForWorkers() throws Exception { }).start(); final DumbSlave worker = jenkinsRule.createOnlineSlave(Label.get("testPipelineWorker")); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -1463,9 +1442,8 @@ public void testCustomHostnameForWorkersInFreestyleJob() throws Exception { } }).start(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(1); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(1); + final List spans = clientStub.getSpans(); assertEquals(1, spans.size()); final TraceSpan buildSpan = spans.get(0); @@ -1555,9 +1533,8 @@ public void testIsManualTrue() throws Exception { CauseAction causeAction = new CauseAction(new TimerTriggerCause(), new UserIdCause("johanna")); job.scheduleBuild2(0, causeAction).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); final String isManual = buildSpan.getMeta().get(CITags.IS_MANUAL); @@ -1597,9 +1574,8 @@ public void testIsManualFalse() throws Exception { CauseAction causeAction = new CauseAction(new TimerTriggerCause(), new SCMTriggerCause("scm")); job.scheduleBuild2(0, causeAction).get(); - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(3); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(3); + final List spans = clientStub.getSpans(); assertEquals(3, spans.size()); final TraceSpan buildSpan = spans.get(0); final String isManual = buildSpan.getMeta().get(CITags.IS_MANUAL); @@ -1736,9 +1712,8 @@ private void givenPipeline(String name, String definitionPath) throws Exception } private List whenExecuting(int expectedSpanCount) throws InterruptedException, TimeoutException { - final FakeTracesHttpClient agentHttpClient = clientStub.agentHttpClient(); - agentHttpClient.waitForTraces(expectedSpanCount); - final List spans = agentHttpClient.getSpans(); + clientStub.waitForTraces(expectedSpanCount); + final List spans = clientStub.getSpans(); assertEquals(expectedSpanCount, spans.size()); return spans; } diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/traces/mapper/JsonTraceSpanMapperTest.java b/src/test/java/org/datadog/jenkins/plugins/datadog/traces/mapper/JsonTraceSpanMapperTest.java index 195d3bb05..543b40fe9 100644 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/traces/mapper/JsonTraceSpanMapperTest.java +++ b/src/test/java/org/datadog/jenkins/plugins/datadog/traces/mapper/JsonTraceSpanMapperTest.java @@ -12,18 +12,14 @@ import static org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper.SPAN_TYPE; import static org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper.START; import static org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper.TRACE_ID; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; -import net.sf.json.JSONArray; +import java.util.Map; import net.sf.json.JSONObject; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; import org.junit.Test; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; - public class JsonTraceSpanMapperTest { public static final JsonTraceSpanMapper sut = new JsonTraceSpanMapper(); @@ -41,14 +37,9 @@ public void testJsonTraceSpanMapper() { span.setEndNano(1000); //When - final JSONArray traces = JSONArray.fromObject(new String(sut.map(Collections.singletonList(span)), StandardCharsets.UTF_8)); + final JSONObject jsonSpan = sut.map(span); //Then - assertEquals(1, traces.size()); - JSONArray trace = (JSONArray) traces.get(0); - assertEquals(1, trace.size()); - - final JSONObject jsonSpan = (JSONObject) trace.get(0); assertNotEquals(0, jsonSpan.get(TRACE_ID)); assertNotEquals(0, jsonSpan.get(SPAN_ID)); assertNotEquals(0, jsonSpan.get(PARENT_ID)); @@ -58,35 +49,9 @@ public void testJsonTraceSpanMapper() { assertEquals("test-service-name", jsonSpan.get(SERVICE_NAME)); assertEquals("test-type", jsonSpan.get(SPAN_TYPE)); assertEquals("meta-value", ((Map)jsonSpan.get(META)).get("meta-key")); - assertEquals(1, ((Map)jsonSpan.get(METRICS)).get("metric-key")); + assertEquals(1.0, ((Map)jsonSpan.get(METRICS)).get("metric-key")); assertEquals(0, jsonSpan.get(START)); assertEquals(1000, jsonSpan.get(DURATION)); } - - @Test - public void testSameTrace() { - //Given - final TraceSpan rootSpan = new TraceSpan("root", 0); - final TraceSpan childSpan = new TraceSpan("child", 0, rootSpan.context()); - - //When - final JSONArray traces = JSONArray.fromObject(new String(sut.map(Arrays.asList(rootSpan, childSpan)), StandardCharsets.UTF_8)); - - //Then - assertEquals(1, traces.size()); - } - - @Test - public void testDifferentTrace() { - //Given - final TraceSpan rootOneSpan = new TraceSpan("rootOne", 0); - final TraceSpan rootTwoSpan = new TraceSpan("rootTwo", 0); - - //When - final JSONArray traces = JSONArray.fromObject(new String(sut.map(Arrays.asList(rootOneSpan, rootTwoSpan)), StandardCharsets.UTF_8)); - - //Then - assertEquals(2, traces.size()); - } } \ No newline at end of file diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/transport/FakeHttpSender.java b/src/test/java/org/datadog/jenkins/plugins/datadog/transport/FakeHttpSender.java deleted file mode 100644 index a13c01903..000000000 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/transport/FakeHttpSender.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; - -public class FakeHttpSender extends HttpSender { - private static final HttpErrorHandler NO_OP = new HttpErrorHandler() { - @Override - public void handle(Exception exception) { - // N/A - } - }; - - private static final Logger log = LoggerFactory.getLogger(FakeHttpSender.class); - - private final List httpMessages = new CopyOnWriteArrayList<>(); - private final List latches = new ArrayList(); - private final AtomicInteger messageCount = new AtomicInteger(); - - FakeHttpSender(BlockingQueue queue) { - super(queue, NO_OP, 1000); - } - - @Override - protected void process(HttpMessage message) { - this.messageCount.incrementAndGet(); - synchronized (this.latches) { - httpMessages.add(message); - for(final CountDownLatch latch : latches) { - if(httpMessages.size() >= latch.getCount()) { - while (latch.getCount() > 0) { - latch.countDown(); - } - } - } - } - } - - public boolean waitForMessagesMax(final int number, int seconds) throws InterruptedException { - final CountDownLatch latch = new CountDownLatch(number); - synchronized (latches) { - if (httpMessages.size() >= number) { - return true; - } - latches.add(latch); - } - return latch.await(seconds, TimeUnit.SECONDS); - } - - public void waitForMessages(final int number) throws InterruptedException, TimeoutException { - if (!waitForMessagesMax(number, 20)) { - String msg = "Timeout waiting for " + number + " message(s). messages.size() == " + httpMessages.size(); - log.warn(msg); - throw new TimeoutException(msg); - } - } - - public List getHttpMessages() { - return httpMessages; - } -} diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/transport/FakeTracesHttpClient.java b/src/test/java/org/datadog/jenkins/plugins/datadog/transport/FakeTracesHttpClient.java deleted file mode 100644 index 17b3a2105..000000000 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/transport/FakeTracesHttpClient.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; - -public class FakeTracesHttpClient implements HttpClient { - private static final Logger log = LoggerFactory.getLogger(FakeTracesHttpClient.class); - - private final List spans = new CopyOnWriteArrayList<>(); - private final List latches = new ArrayList(); - private final AtomicInteger traceCount = new AtomicInteger(); - - @Override - public void send(List messages) { - for(PayloadMessage msg : messages){ - final TraceSpan span = (TraceSpan) msg; - this.traceCount.incrementAndGet(); - synchronized (this.latches) { - spans.add(span); - for(final CountDownLatch latch : latches) { - if(spans.size() >= latch.getCount()) { - while (latch.getCount() > 0) { - latch.countDown(); - } - } - } - } - } - } - - public boolean waitForTracesMax(final int number, int seconds) throws InterruptedException { - final CountDownLatch latch = new CountDownLatch(number); - synchronized (latches) { - if (spans.size() >= number) { - return true; - } - latches.add(latch); - } - return latch.await(seconds, TimeUnit.SECONDS); - } - - public void waitForTraces(final int number) throws InterruptedException, TimeoutException { - if (!waitForTracesMax(number, 20)) { - String msg = "Timeout waiting for " + number + " trace(s). FakeAgentHttpClient.size() == " + spans.size(); - log.warn(msg); - throw new TimeoutException(msg); - } - } - - public List getSpans() { - Collections.sort(spans, new Comparator() { - @Override - public int compare(TraceSpan span1, TraceSpan span2) { - if(span1.getStartNano() < span2.getStartNano()){ - return -1; - } else if (span1.getStartNano() > span2.getStartNano()) { - return 1; - } - return 0; - } - }); - return spans; - } - - @Override - public void stop() { - // N/A - } - - @Override - public void close() { - // N/A - } -} diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/transport/HttpSenderTest.java b/src/test/java/org/datadog/jenkins/plugins/datadog/transport/HttpSenderTest.java deleted file mode 100644 index 1d7df983c..000000000 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/transport/HttpSenderTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.datadog.jenkins.plugins.datadog.transport; - -import static org.junit.Assert.*; - -import org.junit.Test; - -import java.net.URL; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeoutException; - -public class HttpSenderTest { - - private static final HttpMessage SAMPLE_MESSAGE = new HttpMessage(buildURL("http://localhost"), null, null, null); - - @Test - public void testHttpSenderConsumer() throws TimeoutException, InterruptedException { - //Given - final BlockingQueue queue = new LinkedBlockingQueue<>(10); - final FakeHttpSender sender = new FakeHttpSender(queue); - final ExecutorService executor = Executors.newSingleThreadExecutor(); - executor.submit(sender); - - //When - sender.send(SAMPLE_MESSAGE); - sender.send(SAMPLE_MESSAGE); - sender.send(SAMPLE_MESSAGE); - - //Then - sender.waitForMessages(3); - assertEquals(3, sender.getHttpMessages().size()); - } - - - private static URL buildURL(final String urlStr) { - try { - return new URL(urlStr); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } -} \ No newline at end of file From 1419368708b2a2534eb11e160e58f699a7cba898 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko Date: Wed, 24 Jan 2024 12:14:55 +0100 Subject: [PATCH 2/9] Fix webhook tests --- .../plugins/datadog/listeners/DatadogBuildListenerIT.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListenerIT.java b/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListenerIT.java index 3c159bb75..f552a7b71 100644 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListenerIT.java +++ b/src/test/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogBuildListenerIT.java @@ -271,6 +271,8 @@ public void testUserSuppliedGitWithCommitInfo() throws Exception { @Test public void testUserSuppliedGitWithCommitInfoWebhook() throws Exception { + clientStub.configureForWebhooks(); + Jenkins jenkins = jenkinsRule.jenkins; final EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); EnvVars env = prop.getEnvVars(); @@ -373,6 +375,8 @@ public void testFilterSensitiveInfoRepoUrl() throws Exception { @Test public void testGitAlternativeRepoUrlWebhook() throws Exception { + clientStub.configureForWebhooks(); + Jenkins jenkins = jenkinsRule.jenkins; final EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty(); EnvVars env = prop.getEnvVars(); @@ -447,6 +451,8 @@ public void testCITagsOnTraces() throws Exception { @Test public void testCITagsOnWebhooks() throws Exception { + clientStub.configureForWebhooks(); + DatadogGlobalConfiguration cfg = DatadogUtilities.getDatadogGlobalDescriptor(); cfg.setGlobalJobTags("(.*?)_job, global_job_tag:$ENV_VAR"); cfg.setGlobalTags("global_tag:$ENV_VAR"); From ddfef46c8db91cde417747f75722efa3d2de2913 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko Date: Wed, 24 Jan 2024 20:28:28 +0100 Subject: [PATCH 3/9] Address review comments --- .../datadog/traces/write/TraceWriter.java | 83 ++++++++++--------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java index fc532fe05..3611eec8b 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java @@ -22,26 +22,29 @@ public class TraceWriter { private static final String QUEUE_CAPACITY_ENV_VAR = "DD_JENKINS_TRACES_QUEUE_CAPACITY"; private static final String SUBMIT_TIMEOUT_ENV_VAR = "DD_JENKINS_TRACES_SUBMIT_TIMEOUT_SECONDS"; private static final String STOP_TIMEOUT_ENV_VAR = "DD_JENKINS_TRACES_STOP_TIMEOUT_SECONDS"; - private static final String POLLING_INTERVAL_ENV_VAR = "DD_JENKINS_TRACES_POLLING_INTERVAL_SECONDS"; + private static final String POLLING_TIMEOUT_ENV_VAR = "DD_JENKINS_TRACES_POLLING_TIMEOUT_SECONDS"; private static final String BATCH_SIZE_LIMIT_ENV_VAR = "DD_JENKINS_TRACES_BATCH_SIZE_LIMIT"; private static final int DEFAULT_QUEUE_CAPACITY = 10_000; private static final int DEFAULT_SUBMIT_TIMEOUT_SECONDS = 30; - private static final int DEFAULT_STOP_TIMEOUT_SECONDS = 15; - private static final int DEFAULT_POLLING_INTERVAL_SECONDS = 10; + private static final int DEFAULT_STOP_TIMEOUT_SECONDS = 10; + private static final int DEFAULT_POLLING_TIMEOUT_SECONDS = 5; private static final int DEFAULT_BATCH_SIZE_LIMIT = 100; private final TraceWriteStrategy traceWriteStrategy; private final BlockingQueue queue; private final Thread poller; + private final Thread pollerShutdownHook; public TraceWriter(DatadogClient datadogClient) { this.traceWriteStrategy = datadogClient.createTraceWriteStrategy(); this.queue = new ArrayBlockingQueue<>(getEnv(QUEUE_CAPACITY_ENV_VAR, DEFAULT_QUEUE_CAPACITY)); - this.poller = new Thread(new QueuePoller(traceWriteStrategy, queue), "DD-Trace-Writer"); - this.poller.setDaemon(true); + this.poller = new Thread(this::runPollingLoop, "DD-Trace-Writer"); this.poller.start(); + + this.pollerShutdownHook = new Thread(this::runShutdownHook, "DD-Trace-Writer-Shutdown-Hook"); + Runtime.getRuntime().addShutdownHook(pollerShutdownHook); } public void submitBuild(final BuildData buildData, final Run run) throws InterruptedException, TimeoutException { @@ -66,41 +69,47 @@ public void stop() { poller.interrupt(); } - private static final class QueuePoller implements Runnable { - private final TraceWriteStrategy traceWriteStrategy; - private final BlockingQueue queue; + private void runPollingLoop() { + long stopPollingAt = Long.MAX_VALUE; + while (System.currentTimeMillis() < stopPollingAt) { + try { + JSONObject span = queue.poll(getEnv(POLLING_TIMEOUT_ENV_VAR, DEFAULT_POLLING_TIMEOUT_SECONDS), TimeUnit.SECONDS); + if (span == null) { + // nothing to send + continue; + } - public QueuePoller(TraceWriteStrategy traceWriteStrategy, BlockingQueue queue) { - this.traceWriteStrategy = traceWriteStrategy; - this.queue = queue; - } + int batchSize = getEnv(BATCH_SIZE_LIMIT_ENV_VAR, DEFAULT_BATCH_SIZE_LIMIT); + List spans = new ArrayList<>(batchSize); + spans.add(span); + queue.drainTo(spans, batchSize - 1); - @Override - public void run() { - long shutdownAt = Long.MAX_VALUE; - while (System.currentTimeMillis() < shutdownAt) { - try { - JSONObject span = queue.poll(getEnv(POLLING_INTERVAL_ENV_VAR, DEFAULT_POLLING_INTERVAL_SECONDS), TimeUnit.SECONDS); - if (span == null) { - continue; // nothing to send - } - - int batchSize = getEnv(BATCH_SIZE_LIMIT_ENV_VAR, DEFAULT_BATCH_SIZE_LIMIT); - List spans = new ArrayList<>(batchSize); - spans.add(span); - queue.drainTo(spans, batchSize - 1); - - traceWriteStrategy.send(spans); - - } catch (InterruptedException e) { - logger.info("Queue poller thread interrupted"); - shutdownAt = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(getEnv(STOP_TIMEOUT_ENV_VAR, DEFAULT_STOP_TIMEOUT_SECONDS)); - - } catch (Exception e) { - DatadogUtilities.severe(logger, e, "Error while sending trace"); - } + traceWriteStrategy.send(spans); + + } catch (InterruptedException e) { + logger.info("Queue poller thread interrupted"); + stopPollingAt = Math.min(stopPollingAt, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(getEnv(STOP_TIMEOUT_ENV_VAR, DEFAULT_STOP_TIMEOUT_SECONDS))); + + } catch (Exception e) { + DatadogUtilities.severe(logger, e, "Error while sending trace"); } - logger.info("Queue poller thread shut down"); + } + logger.info("Queue polling stopped, spans not flushed: " + queue.size()); + + try { + Runtime.getRuntime().removeShutdownHook(pollerShutdownHook); + } catch (IllegalStateException e) { + // JVM is being shutdown, the hook has already been called + } + } + + private void runShutdownHook() { + stop(); + try { + // delay JVM shutdown until remaining spans are sent (or until timeout) + poller.join(TimeUnit.SECONDS.toMillis(getEnv(STOP_TIMEOUT_ENV_VAR, DEFAULT_STOP_TIMEOUT_SECONDS))); + } catch (InterruptedException e) { + // ignore, should be impossible to end up here } } From 7bf5c20aa0b7cc297451cf80625c940af8269412 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko Date: Wed, 24 Jan 2024 20:52:04 +0100 Subject: [PATCH 4/9] Fix SpotBugs warning --- .../plugins/datadog/traces/write/TraceWriter.java | 15 +++++++++------ .../datadog/traces/write/TraceWriterFactory.java | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java index 3611eec8b..ad1f9782a 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java @@ -15,7 +15,7 @@ import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.jenkinsci.plugins.workflow.graph.FlowNode; -public class TraceWriter { +public final class TraceWriter { private static final Logger logger = Logger.getLogger(TraceWriter.class.getName()); @@ -41,12 +41,19 @@ public TraceWriter(DatadogClient datadogClient) { this.queue = new ArrayBlockingQueue<>(getEnv(QUEUE_CAPACITY_ENV_VAR, DEFAULT_QUEUE_CAPACITY)); this.poller = new Thread(this::runPollingLoop, "DD-Trace-Writer"); - this.poller.start(); this.pollerShutdownHook = new Thread(this::runShutdownHook, "DD-Trace-Writer-Shutdown-Hook"); Runtime.getRuntime().addShutdownHook(pollerShutdownHook); } + public void start() { + poller.start(); + } + + public void stop() { + poller.interrupt(); + } + public void submitBuild(final BuildData buildData, final Run run) throws InterruptedException, TimeoutException { JSONObject buildJson = traceWriteStrategy.serialize(buildData, run); submit(buildJson); @@ -65,10 +72,6 @@ private void submit(JSONObject json) throws InterruptedException, TimeoutExcepti } } - public void stop() { - poller.interrupt(); - } - private void runPollingLoop() { long stopPollingAt = Long.MAX_VALUE; while (System.currentTimeMillis() < stopPollingAt) { diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java index d8691f3dd..5de0bb808 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java @@ -18,6 +18,7 @@ public static synchronized void onDatadogClientUpdate(@Nullable DatadogClient cl } TRACE_WRITER = new TraceWriter(client); + TRACE_WRITER.start(); } @Nullable From e7566bdc6aadfdaa272c5278f7078e59fd90823f Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko Date: Thu, 25 Jan 2024 18:17:06 +0100 Subject: [PATCH 5/9] Replace custom shutdown hook with Jenkins @Terminator mechanism --- .../listeners/DatadogGraphListener.java | 2 +- .../datadog/traces/write/TraceWriter.java | 31 +++++-------------- .../traces/write/TraceWriterFactory.java | 15 ++++++++- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListener.java b/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListener.java index 226216c80..0e3f5f621 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListener.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/listeners/DatadogGraphListener.java @@ -110,7 +110,7 @@ public void onNewHead(FlowNode flowNode) { TraceWriter traceWriter = TraceWriterFactory.getTraceWriter(); if (traceWriter != null) { try { - traceWriter.submitPipeline(flowNode, run); + traceWriter.submitPipelineStep(flowNode, run); } catch (InterruptedException e) { Thread.currentThread().interrupt(); DatadogUtilities.severe(logger, e, "Interrupted while submitting pipeline trace for node " + flowNode.getDisplayName() + " in run " + (run != null ? run.getDisplayName() : "")); diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java index ad1f9782a..f53d77633 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java @@ -33,25 +33,24 @@ public final class TraceWriter { private final TraceWriteStrategy traceWriteStrategy; private final BlockingQueue queue; private final Thread poller; - private final Thread pollerShutdownHook; public TraceWriter(DatadogClient datadogClient) { this.traceWriteStrategy = datadogClient.createTraceWriteStrategy(); - this.queue = new ArrayBlockingQueue<>(getEnv(QUEUE_CAPACITY_ENV_VAR, DEFAULT_QUEUE_CAPACITY)); - this.poller = new Thread(this::runPollingLoop, "DD-Trace-Writer"); - - this.pollerShutdownHook = new Thread(this::runShutdownHook, "DD-Trace-Writer-Shutdown-Hook"); - Runtime.getRuntime().addShutdownHook(pollerShutdownHook); } public void start() { poller.start(); } - public void stop() { + public void stopAsynchronously() { + poller.interrupt(); + } + + public void stopSynchronously() throws InterruptedException { poller.interrupt(); + poller.join(TimeUnit.SECONDS.toMillis(getEnv(STOP_TIMEOUT_ENV_VAR, DEFAULT_STOP_TIMEOUT_SECONDS))); } public void submitBuild(final BuildData buildData, final Run run) throws InterruptedException, TimeoutException { @@ -59,7 +58,7 @@ public void submitBuild(final BuildData buildData, final Run run) throws In submit(buildJson); } - public void submitPipeline(FlowNode flowNode, Run run) throws InterruptedException, TimeoutException { + public void submitPipelineStep(FlowNode flowNode, Run run) throws InterruptedException, TimeoutException { Collection nodeJsons = traceWriteStrategy.serialize(flowNode, run); for (JSONObject nodeJson : nodeJsons) { submit(nodeJson); @@ -98,22 +97,6 @@ private void runPollingLoop() { } } logger.info("Queue polling stopped, spans not flushed: " + queue.size()); - - try { - Runtime.getRuntime().removeShutdownHook(pollerShutdownHook); - } catch (IllegalStateException e) { - // JVM is being shutdown, the hook has already been called - } - } - - private void runShutdownHook() { - stop(); - try { - // delay JVM shutdown until remaining spans are sent (or until timeout) - poller.join(TimeUnit.SECONDS.toMillis(getEnv(STOP_TIMEOUT_ENV_VAR, DEFAULT_STOP_TIMEOUT_SECONDS))); - } catch (InterruptedException e) { - // ignore, should be impossible to end up here - } } private static int getEnv(String envVar, int defaultValue) { diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java index 5de0bb808..71714cee9 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriterFactory.java @@ -1,5 +1,6 @@ package org.datadog.jenkins.plugins.datadog.traces.write; +import hudson.init.Terminator; import javax.annotation.Nullable; import org.datadog.jenkins.plugins.datadog.DatadogClient; import org.datadog.jenkins.plugins.datadog.clients.ClientFactory; @@ -14,13 +15,25 @@ public static synchronized void onDatadogClientUpdate(@Nullable DatadogClient cl } if (TRACE_WRITER != null) { - TRACE_WRITER.stop(); + TRACE_WRITER.stopAsynchronously(); } TRACE_WRITER = new TraceWriter(client); TRACE_WRITER.start(); } + /** + * This method is called when the plugin is stopped. + * If writer is initialized, it will be stopped synchronously. + */ + @Terminator + public static synchronized void stop() throws InterruptedException { + if (TRACE_WRITER != null) { + TRACE_WRITER.stopSynchronously(); + TRACE_WRITER = null; + } + } + @Nullable public static TraceWriter getTraceWriter() { if (TRACE_WRITER == null) { From c793cf89cdfb8384bb97d71639048d679809a85c Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko Date: Fri, 26 Jan 2024 17:43:48 +0100 Subject: [PATCH 6/9] Address review comments --- .../datadog/clients/DatadogAgentClient.java | 50 +++++++++---------- .../datadog/clients/DatadogApiClient.java | 50 ++++++++++--------- .../traces/write/AgentTraceWriteStrategy.java | 47 ++++++++++------- .../plugins/datadog/traces/write/Span.java | 25 ++++++++++ .../traces/write/TraceWriteStrategy.java | 12 +++-- .../traces/write/TraceWriteStrategyImpl.java | 42 +++++++++++----- .../datadog/traces/write/TraceWriter.java | 22 ++++---- .../plugins/datadog/traces/write/Track.java | 3 ++ .../plugins/datadog/util/CircuitBreaker.java | 5 -- .../datadog/clients/DatadogClientStub.java | 30 +++++++---- 10 files changed, 177 insertions(+), 109 deletions(-) create mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Span.java create mode 100644 src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Track.java diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java index b73756625..8507db19b 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java @@ -34,10 +34,10 @@ of this software and associated documentation files (the "Software"), to deal import java.net.Socket; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -49,14 +49,12 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.DatadogEvent; import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; -import org.datadog.jenkins.plugins.datadog.traces.DatadogTraceBuildLogic; -import org.datadog.jenkins.plugins.datadog.traces.DatadogTracePipelineLogic; -import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookBuildLogic; -import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookPipelineLogic; import org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper; import org.datadog.jenkins.plugins.datadog.traces.write.AgentTraceWriteStrategy; +import org.datadog.jenkins.plugins.datadog.traces.write.Span; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategyImpl; +import org.datadog.jenkins.plugins.datadog.traces.write.Track; import org.datadog.jenkins.plugins.datadog.util.SuppressFBWarnings; import org.datadog.jenkins.plugins.datadog.util.TagsUtil; import org.json.JSONArray; @@ -524,8 +522,8 @@ public boolean sendLogs(String payload) { @Override public TraceWriteStrategy createTraceWriteStrategy() { - TraceWriteStrategyImpl evpStrategy = new TraceWriteStrategyImpl(new DatadogWebhookBuildLogic(), new DatadogWebhookPipelineLogic(), this::sendSpansToWebhook); - TraceWriteStrategyImpl apmStrategy = new TraceWriteStrategyImpl(new DatadogTraceBuildLogic(), new DatadogTracePipelineLogic(), this::sendSpansToApm); + TraceWriteStrategyImpl evpStrategy = new TraceWriteStrategyImpl(Track.WEBHOOK, this::sendSpansToWebhook); + TraceWriteStrategyImpl apmStrategy = new TraceWriteStrategyImpl(Track.APM, this::sendSpansToApm); return new AgentTraceWriteStrategy(evpStrategy, apmStrategy, this::isEvpProxySupported); } @@ -538,19 +536,7 @@ boolean isEvpProxySupported() { /** * Posts a given payload to the Agent EVP Proxy, so it is forwarded to the Webhook Intake. */ - private void sendSpansToWebhook(List spans) { - for (net.sf.json.JSONObject span : spans) { - // webhook intake does not support batch requests - postWebhook(span.toString()); - } - } - - /** - * Posts a given payload to the Agent EVP Proxy, so it is forwarded to the Webhook Intake. - */ - private void postWebhook(String payload) { - logger.fine("Sending webhook"); - + private void sendSpansToWebhook(Collection spans) { DatadogGlobalConfiguration datadogGlobalDescriptor = DatadogUtilities.getDatadogGlobalDescriptor(); String urlParameters = datadogGlobalDescriptor != null ? "?service=" + datadogGlobalDescriptor.getCiInstanceName() : ""; String url = String.format("http://%s:%d/evp_proxy/v1/api/v2/webhook/%s", hostname, traceCollectionPort, urlParameters); @@ -559,15 +545,29 @@ private void postWebhook(String payload) { headers.put("X-Datadog-EVP-Subdomain", "webhook-intake"); headers.put("DD-CI-PROVIDER-NAME", "jenkins"); - byte[] body = payload.getBytes(StandardCharsets.UTF_8); - client.postAsynchronously(url, headers, "application/json", body); + for (Span span : spans) { + if (span.getTrack() != Track.WEBHOOK) { + logger.severe("Expected webhook track, got " + span.getTrack() + ", dropping span"); + continue; + } + + byte[] body = span.getPayload().toString().getBytes(StandardCharsets.UTF_8); + + // webhook intake does not support batch requests + logger.fine("Sending webhook"); + client.postAsynchronously(url, headers, "application/json", body); + } } - private void sendSpansToApm(List spans) { + private void sendSpansToApm(Collection spans) { try { Map tracesById = new HashMap<>(); - for (net.sf.json.JSONObject span : spans) { - tracesById.computeIfAbsent(span.getString(JsonTraceSpanMapper.TRACE_ID), k -> new net.sf.json.JSONArray()).add(span); + for (Span span : spans) { + if (span.getTrack() != Track.APM) { + logger.severe("Expected APM track, got " + span.getTrack() + ", dropping span"); + continue; + } + tracesById.computeIfAbsent(span.getPayload().getString(JsonTraceSpanMapper.TRACE_ID), k -> new net.sf.json.JSONArray()).add(span.getPayload()); } final JSONArray jsonTraces = new JSONArray(); 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 e67ccec68..23ccd88df 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 @@ -28,9 +28,9 @@ of this software and associated documentation files (the "Software"), to deal import hudson.util.Secret; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; @@ -45,10 +45,10 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.DatadogEvent; import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; -import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookBuildLogic; -import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookPipelineLogic; +import org.datadog.jenkins.plugins.datadog.traces.write.Span; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategyImpl; +import org.datadog.jenkins.plugins.datadog.traces.write.Track; import org.datadog.jenkins.plugins.datadog.util.SuppressFBWarnings; import org.datadog.jenkins.plugins.datadog.util.TagsUtil; @@ -102,14 +102,13 @@ public class DatadogApiClient implements DatadogClient { public static DatadogClient getInstance(String url, String logIntakeUrl, String webhookIntakeUrl, Secret apiKey){ // If the configuration has not changed, return the current instance without validation // since we've already validated and/or errored about the data - - DatadogApiClient newInstance = new DatadogApiClient(url, logIntakeUrl, webhookIntakeUrl, apiKey); - if (instance != null && instance.equals(newInstance)) { + if (instance != null && !configurationChanged(url, logIntakeUrl, webhookIntakeUrl, apiKey)) { if (DatadogApiClient.failedLastValidation) { return null; } return instance; } + DatadogApiClient newInstance = new DatadogApiClient(url, logIntakeUrl, webhookIntakeUrl, apiKey); if (enableValidations) { synchronized (DatadogApiClient.class) { DatadogApiClient.instance = newInstance; @@ -126,6 +125,13 @@ public static DatadogClient getInstance(String url, String logIntakeUrl, String return newInstance; } + private static boolean configurationChanged(String url, String logIntakeUrl, String webhookIntakeUrl, Secret apiKey){ + return !instance.getUrl().equals(url) || + !instance.getLogIntakeUrl().equals(logIntakeUrl) || + !instance.getWebhookIntakeUrl().equals(webhookIntakeUrl) || + !instance.getApiKey().equals(apiKey); + } + private DatadogApiClient(String url, String logIntakeUrl, String webhookIntakeUrl, Secret apiKey) { this.url = url; this.apiKey = apiKey; @@ -485,24 +491,10 @@ private boolean validateWebhookIntakeConnection() throws IOException { @Override public TraceWriteStrategy createTraceWriteStrategy() { - return new TraceWriteStrategyImpl(new DatadogWebhookBuildLogic(), new DatadogWebhookPipelineLogic(), this::sendSpans); + return new TraceWriteStrategyImpl(Track.WEBHOOK, this::sendSpans); } - private void sendSpans(List spans) { - for (JSONObject span : spans) { - // webhook intake does not support batch requests - postWebhook(span.toString()); - } - } - - /** - * Posts a given payload to the Datadog Webhook Intake, using the user configured apiKey. - * - * @param payload - A webhook payload. - */ - private void postWebhook(String payload) { - logger.fine("Sending webhook"); - + private void sendSpans(Collection spans) { if (this.webhookIntakeConnectionBroken) { throw new RuntimeException("Your client is not initialized properly; webhook intake connection is broken."); } @@ -515,7 +507,17 @@ private void postWebhook(String payload) { headers.put("DD-API-KEY", Secret.toString(apiKey)); headers.put("DD-CI-PROVIDER-NAME", "jenkins"); - byte[] body = payload.getBytes(StandardCharsets.UTF_8); - httpClient.postAsynchronously(url, headers, "application/json", body); + for (Span span : spans) { + if (span.getTrack() != Track.WEBHOOK) { + logger.severe("Expected webhook track, got " + span.getTrack() + ", dropping span"); + continue; + } + + byte[] body = span.getPayload().toString().getBytes(StandardCharsets.UTF_8); + + // webhook intake does not support batch requests + logger.fine("Sending webhook"); + httpClient.postAsynchronously(url, headers, "application/json", body); + } } } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java index 790268f9d..e3c206281 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java @@ -3,10 +3,12 @@ import hudson.model.Run; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.logging.Logger; -import net.sf.json.JSONObject; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; import org.datadog.jenkins.plugins.datadog.clients.DatadogAgentClient; import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.jenkinsci.plugins.workflow.graph.FlowNode; @@ -27,6 +29,14 @@ public class AgentTraceWriteStrategy implements TraceWriteStrategy { private final TraceWriteStrategy evpProxyStrategy; private final TraceWriteStrategy apmStrategy; private final Supplier checkEvpProxySupport; + /** + * Whether the Agent supports EVP Proxy. + *

+ * This value may change from {@code false} to {@code true} if the Agent that this Jenkins talks to gets updated + * (the Agent's support for EVP proxy is checked periodically). + *

+ * We don't handle agent downgrades, so {@code true} to {@code false} change is not possible. + */ private volatile boolean evpProxySupported = false; private volatile long lastEvpProxyCheckTimeMs = 0L; @@ -37,32 +47,33 @@ public AgentTraceWriteStrategy(TraceWriteStrategy evpProxyStrategy, TraceWriteSt } @Override - public JSONObject serialize(BuildData buildData, Run run) { - return getCurrentStrategy().serialize(buildData, run); + public Span createSpan(BuildData buildData, Run run) { + return getCurrentStrategy().createSpan(buildData, run); } + @Nonnull @Override - public Collection serialize(FlowNode flowNode, Run run) { - return getCurrentStrategy().serialize(flowNode, run); + public Collection createSpan(FlowNode flowNode, Run run) { + return getCurrentStrategy().createSpan(flowNode, run); } @Override - public void send(List spans) { - // we have to check serialized spans to know where to send them, + public void send(Collection spans) { + // we have to check the track for every span, // because the serialization strategy might've changed in between serialize() and send() - if (isWebhook(spans)) { - evpProxyStrategy.send(spans); - } else { - apmStrategy.send(spans); - } - } + Map> spansByTrack = spans.stream().collect(Collectors.groupingBy(Span::getTrack)); + for (Map.Entry> e : spansByTrack.entrySet()) { + Track track = e.getKey(); + List trackSpans = e.getValue(); - private boolean isWebhook(List spans) { - if (spans.isEmpty()) { - return false; + if (track == Track.WEBHOOK) { + evpProxyStrategy.send(trackSpans); + } else if (track == Track.APM) { + apmStrategy.send(trackSpans); + } else { + throw new IllegalArgumentException("Unexpected track value: " + track); + } } - JSONObject span = spans.iterator().next(); - return span.get("level") != null; } private TraceWriteStrategy getCurrentStrategy() { diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Span.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Span.java new file mode 100644 index 000000000..bbb7a98f8 --- /dev/null +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Span.java @@ -0,0 +1,25 @@ +package org.datadog.jenkins.plugins.datadog.traces.write; + +import javax.annotation.Nonnull; +import net.sf.json.JSONObject; + +public class Span { + + private final JSONObject payload; + private final Track track; + + public Span(@Nonnull JSONObject payload, @Nonnull Track track) { + this.payload = payload; + this.track = track; + } + + @Nonnull + public JSONObject getPayload() { + return payload; + } + + @Nonnull + public Track getTrack() { + return track; + } +} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java index c73a7f7a4..504cea8d4 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java @@ -2,15 +2,17 @@ import hudson.model.Run; import java.util.Collection; -import java.util.List; -import net.sf.json.JSONObject; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.jenkinsci.plugins.workflow.graph.FlowNode; public interface TraceWriteStrategy { - JSONObject serialize(BuildData buildData, Run run); + @Nullable + Span createSpan(BuildData buildData, Run run); - Collection serialize(FlowNode flowNode, Run run); + @Nonnull + Collection createSpan(FlowNode flowNode, Run run); - void send(List spans); + void send(Collection spans); } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java index ce6e822ae..a74ad61de 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java @@ -2,14 +2,20 @@ import hudson.model.Run; import java.util.Collection; -import java.util.List; +import java.util.Collections; import java.util.function.Consumer; import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; import net.sf.json.JSONObject; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; import org.datadog.jenkins.plugins.datadog.model.BuildData; import org.datadog.jenkins.plugins.datadog.traces.DatadogBaseBuildLogic; import org.datadog.jenkins.plugins.datadog.traces.DatadogBasePipelineLogic; +import org.datadog.jenkins.plugins.datadog.traces.DatadogTraceBuildLogic; +import org.datadog.jenkins.plugins.datadog.traces.DatadogTracePipelineLogic; +import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookBuildLogic; +import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookPipelineLogic; import org.datadog.jenkins.plugins.datadog.util.CircuitBreaker; import org.jenkinsci.plugins.workflow.graph.FlowNode; @@ -17,13 +23,22 @@ public class TraceWriteStrategyImpl implements TraceWriteStrategy { private static final Logger logger = Logger.getLogger(TraceWriteStrategyImpl.class.getName()); + private final Track track; private final DatadogBaseBuildLogic buildLogic; private final DatadogBasePipelineLogic pipelineLogic; - private final CircuitBreaker> sendSpansCircuitBreaker; + private final CircuitBreaker> sendSpansCircuitBreaker; - public TraceWriteStrategyImpl(DatadogBaseBuildLogic buildLogic, DatadogBasePipelineLogic pipelineLogic, Consumer> spansSender) { - this.buildLogic = buildLogic; - this.pipelineLogic = pipelineLogic; + public TraceWriteStrategyImpl(Track track, Consumer> spansSender) { + if (track == Track.APM) { + this.buildLogic = new DatadogTraceBuildLogic(); + this.pipelineLogic = new DatadogTracePipelineLogic(); + } else if (track == Track.WEBHOOK) { + this.buildLogic = new DatadogWebhookBuildLogic(); + this.pipelineLogic = new DatadogWebhookPipelineLogic(); + } else { + throw new IllegalArgumentException("Unexpected track value: " + track); + } + this.track = track; this.sendSpansCircuitBreaker = new CircuitBreaker<>( spansSender, this::logTransportBroken, @@ -32,21 +47,24 @@ public TraceWriteStrategyImpl(DatadogBaseBuildLogic buildLogic, DatadogBasePipel } @Override - public JSONObject serialize(final BuildData buildData, final Run run) { - return buildLogic.finishBuildTrace(buildData, run); + public Span createSpan(final BuildData buildData, final Run run) { + JSONObject buildSpan = buildLogic.finishBuildTrace(buildData, run); + return buildSpan != null ? new Span(buildSpan, track) : null; } + @Nonnull @Override - public Collection serialize(FlowNode flowNode, Run run) { - return pipelineLogic.execute(flowNode, run); + public Collection createSpan(FlowNode flowNode, Run run) { + Collection stepSpans = pipelineLogic.execute(flowNode, run); + return stepSpans.stream().map(payload -> new Span(payload, track)).collect(Collectors.toList()); } @Override - public void send(List spans) { - sendSpansCircuitBreaker.accept(spans); + public void send(Collection serializationResult) { + sendSpansCircuitBreaker.accept(serializationResult); } - private void logTransportBroken(List spans) { + private void logTransportBroken(Collection spans) { logger.fine("Ignoring " + spans.size() + " because transport is broken"); } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java index f53d77633..c536ace20 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java @@ -9,7 +9,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; -import net.sf.json.JSONObject; +import javax.annotation.Nullable; import org.datadog.jenkins.plugins.datadog.DatadogClient; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; import org.datadog.jenkins.plugins.datadog.model.BuildData; @@ -31,7 +31,7 @@ public final class TraceWriter { private static final int DEFAULT_BATCH_SIZE_LIMIT = 100; private final TraceWriteStrategy traceWriteStrategy; - private final BlockingQueue queue; + private final BlockingQueue queue; private final Thread poller; public TraceWriter(DatadogClient datadogClient) { @@ -54,19 +54,19 @@ public void stopSynchronously() throws InterruptedException { } public void submitBuild(final BuildData buildData, final Run run) throws InterruptedException, TimeoutException { - JSONObject buildJson = traceWriteStrategy.serialize(buildData, run); - submit(buildJson); + Span span = traceWriteStrategy.createSpan(buildData, run); + submit(span); } public void submitPipelineStep(FlowNode flowNode, Run run) throws InterruptedException, TimeoutException { - Collection nodeJsons = traceWriteStrategy.serialize(flowNode, run); - for (JSONObject nodeJson : nodeJsons) { - submit(nodeJson); + Collection spans = traceWriteStrategy.createSpan(flowNode, run); + for (Span span : spans) { + submit(span); } } - private void submit(JSONObject json) throws InterruptedException, TimeoutException { - if (!queue.offer(json, getEnv(SUBMIT_TIMEOUT_ENV_VAR, DEFAULT_SUBMIT_TIMEOUT_SECONDS), TimeUnit.SECONDS)) { + private void submit(@Nullable Span span) throws InterruptedException, TimeoutException { + if (span != null && !queue.offer(span, getEnv(SUBMIT_TIMEOUT_ENV_VAR, DEFAULT_SUBMIT_TIMEOUT_SECONDS), TimeUnit.SECONDS)) { throw new TimeoutException("Timed out while submitting span"); } } @@ -75,14 +75,14 @@ private void runPollingLoop() { long stopPollingAt = Long.MAX_VALUE; while (System.currentTimeMillis() < stopPollingAt) { try { - JSONObject span = queue.poll(getEnv(POLLING_TIMEOUT_ENV_VAR, DEFAULT_POLLING_TIMEOUT_SECONDS), TimeUnit.SECONDS); + Span span = queue.poll(getEnv(POLLING_TIMEOUT_ENV_VAR, DEFAULT_POLLING_TIMEOUT_SECONDS), TimeUnit.SECONDS); if (span == null) { // nothing to send continue; } int batchSize = getEnv(BATCH_SIZE_LIMIT_ENV_VAR, DEFAULT_BATCH_SIZE_LIMIT); - List spans = new ArrayList<>(batchSize); + List spans = new ArrayList<>(batchSize); spans.add(span); queue.drainTo(spans, batchSize - 1); diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Track.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Track.java new file mode 100644 index 000000000..5114713be --- /dev/null +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Track.java @@ -0,0 +1,3 @@ +package org.datadog.jenkins.plugins.datadog.traces.write; + +public enum Track {APM, WEBHOOK} diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/util/CircuitBreaker.java b/src/main/java/org/datadog/jenkins/plugins/datadog/util/CircuitBreaker.java index 0cd5d5126..5b1f31da1 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/util/CircuitBreaker.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/util/CircuitBreaker.java @@ -75,9 +75,4 @@ public synchronized void accept(T t) { fallback.accept(t); } } - - public synchronized void reset() { - healthy = true; - healthCheckDelayMillis = minHealthCheckDelayMillis; - } } diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java b/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java index 098351110..36c810096 100644 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java +++ b/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java @@ -38,6 +38,7 @@ of this software and associated documentation files (the "Software"), to deal import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import javax.annotation.Nonnull; import net.sf.json.JSONObject; import org.datadog.jenkins.plugins.datadog.DatadogClient; import org.datadog.jenkins.plugins.datadog.DatadogEvent; @@ -48,7 +49,9 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookPipelineLogic; import org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; +import org.datadog.jenkins.plugins.datadog.traces.write.Span; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; +import org.datadog.jenkins.plugins.datadog.traces.write.Track; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.junit.Assert; @@ -265,34 +268,43 @@ private static final class StubTraceWriteStrategy implements TraceWriteStrategy private final Collection webhooks = new LinkedBlockingQueue<>(); @Override - public JSONObject serialize(BuildData buildData, Run run) { + public Span createSpan(BuildData buildData, Run run) { if (isWebhook) { JSONObject json = new DatadogWebhookBuildLogic().finishBuildTrace(buildData, run); + if (json == null) { + return null; + } webhooks.add(json); - return json; + return new Span(json, Track.WEBHOOK); } else { TraceSpan span = new DatadogTraceBuildLogic().createSpan(buildData, run); + if (span == null) { + return null; + } traces.add(span); - return new JsonTraceSpanMapper().map(span); + JSONObject json = new JsonTraceSpanMapper().map(span); + return new Span(json, Track.APM); } } + @Nonnull @Override - public Collection serialize(FlowNode flowNode, Run run) { + public Collection createSpan(FlowNode flowNode, Run run) { if (isWebhook) { - Collection spans = new DatadogWebhookPipelineLogic().execute(flowNode, run); - webhooks.addAll(spans); - return spans; + Collection jsons = new DatadogWebhookPipelineLogic().execute(flowNode, run); + webhooks.addAll(jsons); + return jsons.stream().map(payload -> new Span(payload, Track.WEBHOOK)).collect(Collectors.toList()); } else { Collection traceSpans = new DatadogTracePipelineLogic().collectTraces(flowNode, run); traces.addAll(traceSpans); JsonTraceSpanMapper mapper = new JsonTraceSpanMapper(); - return traceSpans.stream().map(mapper::map).collect(Collectors.toList()); + List jsons = traceSpans.stream().map(mapper::map).collect(Collectors.toList()); + return jsons.stream().map(payload -> new Span(payload, Track.APM)).collect(Collectors.toList()); } } @Override - public void send(List spans) { + public void send(Collection spans) { // no op } From eeeb4c2b3315d985601df1fe375722985fba1044 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko Date: Fri, 26 Jan 2024 17:50:33 +0100 Subject: [PATCH 7/9] Fix NPE --- .../plugins/datadog/clients/DatadogApiClient.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 23ccd88df..0fba0167e 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 @@ -25,6 +25,7 @@ of this software and associated documentation files (the "Software"), to deal package org.datadog.jenkins.plugins.datadog.clients; +import com.google.common.base.Objects; import hudson.util.Secret; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -126,10 +127,10 @@ public static DatadogClient getInstance(String url, String logIntakeUrl, String } private static boolean configurationChanged(String url, String logIntakeUrl, String webhookIntakeUrl, Secret apiKey){ - return !instance.getUrl().equals(url) || - !instance.getLogIntakeUrl().equals(logIntakeUrl) || - !instance.getWebhookIntakeUrl().equals(webhookIntakeUrl) || - !instance.getApiKey().equals(apiKey); + return !Objects.equal(instance.getUrl(), url) || + !Objects.equal(instance.getLogIntakeUrl(), logIntakeUrl) || + !Objects.equal(instance.getWebhookIntakeUrl(), webhookIntakeUrl) || + !Objects.equal(instance.getApiKey(), apiKey); } private DatadogApiClient(String url, String logIntakeUrl, String webhookIntakeUrl, Secret apiKey) { From 1756c79e9c01cdfb53dbeada08747d9b6bcde385 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko Date: Fri, 26 Jan 2024 18:05:39 +0100 Subject: [PATCH 8/9] Renamed some classes and methods --- .../datadog/clients/DatadogAgentClient.java | 14 +++++++------- .../datadog/clients/DatadogApiClient.java | 8 ++++---- .../traces/write/AgentTraceWriteStrategy.java | 16 ++++++++-------- .../traces/write/{Span.java => Payload.java} | 12 ++++++------ .../traces/write/TraceWriteStrategy.java | 6 +++--- .../traces/write/TraceWriteStrategyImpl.java | 17 ++++++++--------- .../datadog/traces/write/TraceWriter.java | 14 +++++++------- .../plugins/datadog/traces/write/Track.java | 4 +++- .../datadog/clients/DatadogClientStub.java | 16 ++++++++-------- 9 files changed, 54 insertions(+), 53 deletions(-) rename src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/{Span.java => Payload.java} (57%) diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java index 8507db19b..9fab0011c 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java @@ -51,7 +51,7 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.DatadogUtilities; import org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper; import org.datadog.jenkins.plugins.datadog.traces.write.AgentTraceWriteStrategy; -import org.datadog.jenkins.plugins.datadog.traces.write.Span; +import org.datadog.jenkins.plugins.datadog.traces.write.Payload; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategyImpl; import org.datadog.jenkins.plugins.datadog.traces.write.Track; @@ -536,7 +536,7 @@ boolean isEvpProxySupported() { /** * Posts a given payload to the Agent EVP Proxy, so it is forwarded to the Webhook Intake. */ - private void sendSpansToWebhook(Collection spans) { + private void sendSpansToWebhook(Collection spans) { DatadogGlobalConfiguration datadogGlobalDescriptor = DatadogUtilities.getDatadogGlobalDescriptor(); String urlParameters = datadogGlobalDescriptor != null ? "?service=" + datadogGlobalDescriptor.getCiInstanceName() : ""; String url = String.format("http://%s:%d/evp_proxy/v1/api/v2/webhook/%s", hostname, traceCollectionPort, urlParameters); @@ -545,13 +545,13 @@ private void sendSpansToWebhook(Collection spans) { headers.put("X-Datadog-EVP-Subdomain", "webhook-intake"); headers.put("DD-CI-PROVIDER-NAME", "jenkins"); - for (Span span : spans) { + for (Payload span : spans) { if (span.getTrack() != Track.WEBHOOK) { logger.severe("Expected webhook track, got " + span.getTrack() + ", dropping span"); continue; } - byte[] body = span.getPayload().toString().getBytes(StandardCharsets.UTF_8); + byte[] body = span.getJson().toString().getBytes(StandardCharsets.UTF_8); // webhook intake does not support batch requests logger.fine("Sending webhook"); @@ -559,15 +559,15 @@ private void sendSpansToWebhook(Collection spans) { } } - private void sendSpansToApm(Collection spans) { + private void sendSpansToApm(Collection spans) { try { Map tracesById = new HashMap<>(); - for (Span span : spans) { + for (Payload span : spans) { if (span.getTrack() != Track.APM) { logger.severe("Expected APM track, got " + span.getTrack() + ", dropping span"); continue; } - tracesById.computeIfAbsent(span.getPayload().getString(JsonTraceSpanMapper.TRACE_ID), k -> new net.sf.json.JSONArray()).add(span.getPayload()); + tracesById.computeIfAbsent(span.getJson().getString(JsonTraceSpanMapper.TRACE_ID), k -> new net.sf.json.JSONArray()).add(span.getJson()); } final JSONArray jsonTraces = new JSONArray(); 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 0fba0167e..2573dbe1f 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 @@ -46,7 +46,7 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.DatadogEvent; import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration; import org.datadog.jenkins.plugins.datadog.DatadogUtilities; -import org.datadog.jenkins.plugins.datadog.traces.write.Span; +import org.datadog.jenkins.plugins.datadog.traces.write.Payload; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategyImpl; import org.datadog.jenkins.plugins.datadog.traces.write.Track; @@ -495,7 +495,7 @@ public TraceWriteStrategy createTraceWriteStrategy() { return new TraceWriteStrategyImpl(Track.WEBHOOK, this::sendSpans); } - private void sendSpans(Collection spans) { + private void sendSpans(Collection spans) { if (this.webhookIntakeConnectionBroken) { throw new RuntimeException("Your client is not initialized properly; webhook intake connection is broken."); } @@ -508,13 +508,13 @@ private void sendSpans(Collection spans) { headers.put("DD-API-KEY", Secret.toString(apiKey)); headers.put("DD-CI-PROVIDER-NAME", "jenkins"); - for (Span span : spans) { + for (Payload span : spans) { if (span.getTrack() != Track.WEBHOOK) { logger.severe("Expected webhook track, got " + span.getTrack() + ", dropping span"); continue; } - byte[] body = span.getPayload().toString().getBytes(StandardCharsets.UTF_8); + byte[] body = span.getJson().toString().getBytes(StandardCharsets.UTF_8); // webhook intake does not support batch requests logger.fine("Sending webhook"); diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java index e3c206281..04d875f54 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/AgentTraceWriteStrategy.java @@ -47,24 +47,24 @@ public AgentTraceWriteStrategy(TraceWriteStrategy evpProxyStrategy, TraceWriteSt } @Override - public Span createSpan(BuildData buildData, Run run) { - return getCurrentStrategy().createSpan(buildData, run); + public Payload serialize(BuildData buildData, Run run) { + return getCurrentStrategy().serialize(buildData, run); } @Nonnull @Override - public Collection createSpan(FlowNode flowNode, Run run) { - return getCurrentStrategy().createSpan(flowNode, run); + public Collection serialize(FlowNode flowNode, Run run) { + return getCurrentStrategy().serialize(flowNode, run); } @Override - public void send(Collection spans) { + public void send(Collection spans) { // we have to check the track for every span, // because the serialization strategy might've changed in between serialize() and send() - Map> spansByTrack = spans.stream().collect(Collectors.groupingBy(Span::getTrack)); - for (Map.Entry> e : spansByTrack.entrySet()) { + Map> spansByTrack = spans.stream().collect(Collectors.groupingBy(Payload::getTrack)); + for (Map.Entry> e : spansByTrack.entrySet()) { Track track = e.getKey(); - List trackSpans = e.getValue(); + List trackSpans = e.getValue(); if (track == Track.WEBHOOK) { evpProxyStrategy.send(trackSpans); diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Span.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Payload.java similarity index 57% rename from src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Span.java rename to src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Payload.java index bbb7a98f8..d9c05fd13 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Span.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Payload.java @@ -3,19 +3,19 @@ import javax.annotation.Nonnull; import net.sf.json.JSONObject; -public class Span { +public class Payload { - private final JSONObject payload; + private final JSONObject json; private final Track track; - public Span(@Nonnull JSONObject payload, @Nonnull Track track) { - this.payload = payload; + public Payload(@Nonnull JSONObject json, @Nonnull Track track) { + this.json = json; this.track = track; } @Nonnull - public JSONObject getPayload() { - return payload; + public JSONObject getJson() { + return json; } @Nonnull diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java index 504cea8d4..ce35751fd 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategy.java @@ -9,10 +9,10 @@ public interface TraceWriteStrategy { @Nullable - Span createSpan(BuildData buildData, Run run); + Payload serialize(BuildData buildData, Run run); @Nonnull - Collection createSpan(FlowNode flowNode, Run run); + Collection serialize(FlowNode flowNode, Run run); - void send(Collection spans); + void send(Collection spans); } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java index a74ad61de..39cbd4a96 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriteStrategyImpl.java @@ -2,7 +2,6 @@ import hudson.model.Run; import java.util.Collection; -import java.util.Collections; import java.util.function.Consumer; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -26,9 +25,9 @@ public class TraceWriteStrategyImpl implements TraceWriteStrategy { private final Track track; private final DatadogBaseBuildLogic buildLogic; private final DatadogBasePipelineLogic pipelineLogic; - private final CircuitBreaker> sendSpansCircuitBreaker; + private final CircuitBreaker> sendSpansCircuitBreaker; - public TraceWriteStrategyImpl(Track track, Consumer> spansSender) { + public TraceWriteStrategyImpl(Track track, Consumer> spansSender) { if (track == Track.APM) { this.buildLogic = new DatadogTraceBuildLogic(); this.pipelineLogic = new DatadogTracePipelineLogic(); @@ -47,24 +46,24 @@ public TraceWriteStrategyImpl(Track track, Consumer> spansSende } @Override - public Span createSpan(final BuildData buildData, final Run run) { + public Payload serialize(final BuildData buildData, final Run run) { JSONObject buildSpan = buildLogic.finishBuildTrace(buildData, run); - return buildSpan != null ? new Span(buildSpan, track) : null; + return buildSpan != null ? new Payload(buildSpan, track) : null; } @Nonnull @Override - public Collection createSpan(FlowNode flowNode, Run run) { + public Collection serialize(FlowNode flowNode, Run run) { Collection stepSpans = pipelineLogic.execute(flowNode, run); - return stepSpans.stream().map(payload -> new Span(payload, track)).collect(Collectors.toList()); + return stepSpans.stream().map(payload -> new Payload(payload, track)).collect(Collectors.toList()); } @Override - public void send(Collection serializationResult) { + public void send(Collection serializationResult) { sendSpansCircuitBreaker.accept(serializationResult); } - private void logTransportBroken(Collection spans) { + private void logTransportBroken(Collection spans) { logger.fine("Ignoring " + spans.size() + " because transport is broken"); } diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java index c536ace20..7d911a635 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/TraceWriter.java @@ -31,7 +31,7 @@ public final class TraceWriter { private static final int DEFAULT_BATCH_SIZE_LIMIT = 100; private final TraceWriteStrategy traceWriteStrategy; - private final BlockingQueue queue; + private final BlockingQueue queue; private final Thread poller; public TraceWriter(DatadogClient datadogClient) { @@ -54,18 +54,18 @@ public void stopSynchronously() throws InterruptedException { } public void submitBuild(final BuildData buildData, final Run run) throws InterruptedException, TimeoutException { - Span span = traceWriteStrategy.createSpan(buildData, run); + Payload span = traceWriteStrategy.serialize(buildData, run); submit(span); } public void submitPipelineStep(FlowNode flowNode, Run run) throws InterruptedException, TimeoutException { - Collection spans = traceWriteStrategy.createSpan(flowNode, run); - for (Span span : spans) { + Collection spans = traceWriteStrategy.serialize(flowNode, run); + for (Payload span : spans) { submit(span); } } - private void submit(@Nullable Span span) throws InterruptedException, TimeoutException { + private void submit(@Nullable Payload span) throws InterruptedException, TimeoutException { if (span != null && !queue.offer(span, getEnv(SUBMIT_TIMEOUT_ENV_VAR, DEFAULT_SUBMIT_TIMEOUT_SECONDS), TimeUnit.SECONDS)) { throw new TimeoutException("Timed out while submitting span"); } @@ -75,14 +75,14 @@ private void runPollingLoop() { long stopPollingAt = Long.MAX_VALUE; while (System.currentTimeMillis() < stopPollingAt) { try { - Span span = queue.poll(getEnv(POLLING_TIMEOUT_ENV_VAR, DEFAULT_POLLING_TIMEOUT_SECONDS), TimeUnit.SECONDS); + Payload span = queue.poll(getEnv(POLLING_TIMEOUT_ENV_VAR, DEFAULT_POLLING_TIMEOUT_SECONDS), TimeUnit.SECONDS); if (span == null) { // nothing to send continue; } int batchSize = getEnv(BATCH_SIZE_LIMIT_ENV_VAR, DEFAULT_BATCH_SIZE_LIMIT); - List spans = new ArrayList<>(batchSize); + List spans = new ArrayList<>(batchSize); spans.add(span); queue.drainTo(spans, batchSize - 1); diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Track.java b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Track.java index 5114713be..e3b756536 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Track.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/traces/write/Track.java @@ -1,3 +1,5 @@ package org.datadog.jenkins.plugins.datadog.traces.write; -public enum Track {APM, WEBHOOK} +public enum Track { + APM, WEBHOOK +} diff --git a/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java b/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java index 36c810096..0fd5e589e 100644 --- a/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java +++ b/src/test/java/org/datadog/jenkins/plugins/datadog/clients/DatadogClientStub.java @@ -49,7 +49,7 @@ of this software and associated documentation files (the "Software"), to deal import org.datadog.jenkins.plugins.datadog.traces.DatadogWebhookPipelineLogic; import org.datadog.jenkins.plugins.datadog.traces.mapper.JsonTraceSpanMapper; import org.datadog.jenkins.plugins.datadog.traces.message.TraceSpan; -import org.datadog.jenkins.plugins.datadog.traces.write.Span; +import org.datadog.jenkins.plugins.datadog.traces.write.Payload; import org.datadog.jenkins.plugins.datadog.traces.write.TraceWriteStrategy; import org.datadog.jenkins.plugins.datadog.traces.write.Track; import org.jenkinsci.plugins.workflow.graph.FlowNode; @@ -268,14 +268,14 @@ private static final class StubTraceWriteStrategy implements TraceWriteStrategy private final Collection webhooks = new LinkedBlockingQueue<>(); @Override - public Span createSpan(BuildData buildData, Run run) { + public Payload serialize(BuildData buildData, Run run) { if (isWebhook) { JSONObject json = new DatadogWebhookBuildLogic().finishBuildTrace(buildData, run); if (json == null) { return null; } webhooks.add(json); - return new Span(json, Track.WEBHOOK); + return new Payload(json, Track.WEBHOOK); } else { TraceSpan span = new DatadogTraceBuildLogic().createSpan(buildData, run); if (span == null) { @@ -283,28 +283,28 @@ public Span createSpan(BuildData buildData, Run run) { } traces.add(span); JSONObject json = new JsonTraceSpanMapper().map(span); - return new Span(json, Track.APM); + return new Payload(json, Track.APM); } } @Nonnull @Override - public Collection createSpan(FlowNode flowNode, Run run) { + public Collection serialize(FlowNode flowNode, Run run) { if (isWebhook) { Collection jsons = new DatadogWebhookPipelineLogic().execute(flowNode, run); webhooks.addAll(jsons); - return jsons.stream().map(payload -> new Span(payload, Track.WEBHOOK)).collect(Collectors.toList()); + return jsons.stream().map(payload -> new Payload(payload, Track.WEBHOOK)).collect(Collectors.toList()); } else { Collection traceSpans = new DatadogTracePipelineLogic().collectTraces(flowNode, run); traces.addAll(traceSpans); JsonTraceSpanMapper mapper = new JsonTraceSpanMapper(); List jsons = traceSpans.stream().map(mapper::map).collect(Collectors.toList()); - return jsons.stream().map(payload -> new Span(payload, Track.APM)).collect(Collectors.toList()); + return jsons.stream().map(payload -> new Payload(payload, Track.APM)).collect(Collectors.toList()); } } @Override - public void send(Collection spans) { + public void send(Collection spans) { // no op } From 885ae5fad09bce420416eab7e852da9ae32491d0 Mon Sep 17 00:00:00 2001 From: Nikita Tkachenko Date: Mon, 29 Jan 2024 11:26:42 +0100 Subject: [PATCH 9/9] Add max payload size check for webhook requests --- .../jenkins/plugins/datadog/clients/DatadogAgentClient.java | 6 ++++++ .../jenkins/plugins/datadog/clients/DatadogApiClient.java | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java index 9fab0011c..90f87a48c 100644 --- a/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java +++ b/src/main/java/org/datadog/jenkins/plugins/datadog/clients/DatadogAgentClient.java @@ -66,6 +66,8 @@ of this software and associated documentation files (the "Software"), to deal */ public class DatadogAgentClient implements DatadogClient { + private static final int PAYLOAD_SIZE_LIMIT = 5 * 1024 * 1024; // 5 MB + private static volatile DatadogAgentClient instance = null; // Used to determine if the instance failed last validation last time, so // we do not keep retrying to create the instance and logging the same error @@ -552,6 +554,10 @@ private void sendSpansToWebhook(Collection spans) { } byte[] body = span.getJson().toString().getBytes(StandardCharsets.UTF_8); + if (body.length > PAYLOAD_SIZE_LIMIT) { + logger.severe("Dropping span because payload size (" + body.length + ") exceeds the allowed limit of " + PAYLOAD_SIZE_LIMIT); + continue; + } // webhook intake does not support batch requests logger.fine("Sending webhook"); 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 2573dbe1f..288137cfd 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 @@ -59,6 +59,8 @@ of this software and associated documentation files (the "Software"), to deal */ public class DatadogApiClient implements DatadogClient { + private static final int PAYLOAD_SIZE_LIMIT = 5 * 1024 * 1024; // 5 MB + private static volatile DatadogApiClient instance = null; // Used to determine if the instance failed last validation last time, so // we do not keep retrying to create the instance and logging the same error @@ -515,6 +517,10 @@ private void sendSpans(Collection spans) { } byte[] body = span.getJson().toString().getBytes(StandardCharsets.UTF_8); + if (body.length > PAYLOAD_SIZE_LIMIT) { + logger.severe("Dropping span because payload size (" + body.length + ") exceeds the allowed limit of " + PAYLOAD_SIZE_LIMIT); + continue; + } // webhook intake does not support batch requests logger.fine("Sending webhook");