-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for automatic APM tracers configuration #354
Merged
nikita-tkachenko-datadog
merged 14 commits into
master
from
nikita-tkachenko/tracer-autoconfig
Oct 13, 2023
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
dc8d66d
Implement Java tracer auto-configuration
3a4643b
Add possibility to specify additional tracer settings
7753611
Cosmetic improvements
648b4c4
Update plugin documentation
a64e143
Add possibility to configure tracer for JS and Python
0cc32d7
Minor documentation update
nikita-tkachenko-datadog a32f30d
Address review comments & add integration tests
nikita-tkachenko-datadog da81348
Address review comments and fix minor issues
nikita-tkachenko-datadog 81ef5b5
Fix tracer injection tests to run on Windows
nikita-tkachenko-datadog d455e67
Fix tracer injection config serialization
nikita-tkachenko-datadog b679939
Add signature verification when downloading Java tracer from Maven Ce…
nikita-tkachenko-datadog 6290fa1
Address Spotbugs warning
nikita-tkachenko-datadog b53ace7
Remove Application Key configuration since the backend no longer requ…
nikita-tkachenko-datadog a36066d
Fix post-merge compilation issue
nikita-tkachenko-datadog File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
174 changes: 174 additions & 0 deletions
174
src/main/java/org/datadog/jenkins/plugins/datadog/tracer/DatadogTracerConfigurator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
package org.datadog.jenkins.plugins.datadog.tracer; | ||
|
||
import hudson.FilePath; | ||
import hudson.model.AbstractBuild; | ||
import hudson.model.AbstractProject; | ||
import hudson.model.InvisibleAction; | ||
import hudson.model.Job; | ||
import hudson.model.Node; | ||
import hudson.model.Run; | ||
import hudson.model.TopLevelItem; | ||
import hudson.util.Secret; | ||
import java.net.MalformedURLException; | ||
import java.net.URI; | ||
import java.net.URISyntaxException; | ||
import java.net.URL; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.EnumMap; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import org.datadog.jenkins.plugins.datadog.DatadogClient; | ||
import org.datadog.jenkins.plugins.datadog.DatadogGlobalConfiguration; | ||
import org.datadog.jenkins.plugins.datadog.DatadogUtilities; | ||
|
||
public class DatadogTracerConfigurator { | ||
|
||
private static final Logger LOGGER = Logger.getLogger(DatadogTracerConfigurator.class.getName()); | ||
|
||
private final Map<TracerLanguage, TracerConfigurator> configurators; | ||
|
||
public static final DatadogTracerConfigurator INSTANCE = new DatadogTracerConfigurator(); | ||
|
||
public DatadogTracerConfigurator() { | ||
configurators = new EnumMap<>(TracerLanguage.class); | ||
configurators.put(TracerLanguage.JAVA, new JavaConfigurator()); | ||
configurators.put(TracerLanguage.JAVASCRIPT, new JavascriptConfigurator()); | ||
configurators.put(TracerLanguage.PYTHON, new PythonConfigurator()); | ||
} | ||
|
||
public Map<String, String> configure(Run<?, ?> run, Node node, Map<String, String> envs) { | ||
Job<?, ?> job = run.getParent(); | ||
DatadogTracerJobProperty<?> tracerConfig = job.getProperty(DatadogTracerJobProperty.class); | ||
if (tracerConfig == null || !tracerConfig.isOn()) { | ||
return Collections.emptyMap(); | ||
} | ||
|
||
Collection<TracerLanguage> languages = tracerConfig.getLanguages(); | ||
for (ConfigureTracerAction action : run.getActions(ConfigureTracerAction.class)) { | ||
if (action.node == node && languages.equals(action.languages)) { | ||
return action.variables; | ||
} | ||
} | ||
|
||
DatadogGlobalConfiguration datadogConfig = DatadogUtilities.getDatadogGlobalDescriptor(); | ||
if (datadogConfig == null) { | ||
LOGGER.log(Level.WARNING, "Cannot set up tracer: Datadog config not found"); | ||
return Collections.emptyMap(); | ||
} | ||
|
||
TopLevelItem topLevelItem = getTopLevelItem(run); | ||
FilePath workspacePath = node.getWorkspaceFor(topLevelItem); | ||
if (workspacePath == null) { | ||
throw new IllegalStateException("Cannot find workspace path for " + topLevelItem + " on " + node); | ||
} | ||
|
||
Map<String, String> variables = new HashMap<>(getCommonEnvVariables(datadogConfig, tracerConfig)); | ||
for (TracerLanguage language : languages) { | ||
TracerConfigurator tracerConfigurator = configurators.get(language); | ||
if (tracerConfigurator == null) { | ||
LOGGER.log(Level.WARNING, "Cannot find tracer configurator for " + language); | ||
continue; | ||
} | ||
|
||
try { | ||
Map<String, String> languageVariables = tracerConfigurator.configure(tracerConfig, node, workspacePath, envs); | ||
variables.putAll(languageVariables); | ||
} catch (Exception e) { | ||
LOGGER.log(Level.WARNING, "Error while configuring " + language + " Datadog Tracer for run " + run + " and node " + node, e); | ||
return Collections.emptyMap(); | ||
} | ||
} | ||
run.addAction(new ConfigureTracerAction(node, languages, variables)); | ||
return variables; | ||
} | ||
|
||
private static TopLevelItem getTopLevelItem(Run<?, ?> run) { | ||
if (run instanceof AbstractBuild) { | ||
AbstractBuild<?, ?> build = (AbstractBuild<?, ?>) run; | ||
AbstractProject<?, ?> project = build.getProject(); | ||
if (project instanceof TopLevelItem) { | ||
return (TopLevelItem) project; | ||
} else { | ||
throw new IllegalArgumentException("Unexpected type of project: " + project); | ||
} | ||
} else { | ||
Job<?, ?> parent = run.getParent(); | ||
if (parent instanceof TopLevelItem) { | ||
return (TopLevelItem) parent; | ||
} else { | ||
throw new IllegalArgumentException("Unexpected type of run parent: " + parent); | ||
} | ||
} | ||
} | ||
|
||
private static Map<String, String> getCommonEnvVariables(DatadogGlobalConfiguration datadogConfig, | ||
DatadogTracerJobProperty<?> tracerConfig) { | ||
Map<String, String> variables = new HashMap<>(); | ||
variables.put("DD_CIVISIBILITY_ENABLED", "true"); | ||
variables.put("DD_ENV", "ci"); | ||
variables.put("DD_SERVICE", tracerConfig.getServiceName()); | ||
|
||
DatadogClient.ClientType clientType = DatadogClient.ClientType.valueOf(datadogConfig.getReportWith()); | ||
switch (clientType) { | ||
case HTTP: | ||
variables.put("DD_CIVISIBILITY_AGENTLESS_ENABLED", "true"); | ||
variables.put("DD_SITE", getSite(datadogConfig.getTargetApiURL())); | ||
variables.put("DD_API_KEY", Secret.toString(datadogConfig.getUsedApiKey())); | ||
break; | ||
case DSD: | ||
variables.put("DD_AGENT_HOST", datadogConfig.getTargetHost()); | ||
variables.put("DD_TRACE_AGENT_PORT", getAgentPort(datadogConfig.getTargetTraceCollectionPort())); | ||
break; | ||
default: | ||
throw new IllegalArgumentException("Unexpected client type: " + clientType); | ||
} | ||
|
||
Map<String, String> additionalVariables = tracerConfig.getAdditionalVariables(); | ||
if (additionalVariables != null) { | ||
variables.putAll(additionalVariables); | ||
} | ||
|
||
return variables; | ||
} | ||
|
||
private static String getSite(String apiUrl) { | ||
// what users configure for Pipelines looks like "https://api.datadoghq.com/api/" | ||
// while what the tracer needs "datadoghq.com" | ||
try { | ||
URI uri = new URL(apiUrl).toURI(); | ||
String host = uri.getHost(); | ||
if (host == null) { | ||
throw new IllegalArgumentException("Cannot find host in Datadog API URL: " + uri); | ||
} | ||
|
||
String[] parts = host.split("\\."); | ||
return (parts.length >= 2 ? parts[parts.length - 2] + "." : "") + parts[parts.length - 1]; | ||
|
||
} catch (MalformedURLException | URISyntaxException e) { | ||
throw new IllegalArgumentException("Cannot parse Datadog API URL", e); | ||
} | ||
} | ||
|
||
private static String getAgentPort(Integer traceCollectionPort) { | ||
if (traceCollectionPort == null) { | ||
throw new IllegalArgumentException("Traces collection port is not set"); | ||
} else { | ||
return traceCollectionPort.toString(); | ||
} | ||
} | ||
|
||
private static final class ConfigureTracerAction extends InvisibleAction { | ||
private final Node node; | ||
private final Collection<TracerLanguage> languages; | ||
private final Map<String, String> variables; | ||
|
||
private ConfigureTracerAction(Node node, Collection<TracerLanguage> languages, Map<String, String> variables) { | ||
this.node = node; | ||
this.languages = languages; | ||
this.variables = variables; | ||
} | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
.../java/org/datadog/jenkins/plugins/datadog/tracer/DatadogTracerEnvironmentContributor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package org.datadog.jenkins.plugins.datadog.tracer; | ||
|
||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import hudson.EnvVars; | ||
import hudson.Extension; | ||
import hudson.model.EnvironmentContributor; | ||
import hudson.model.Executor; | ||
import hudson.model.Node; | ||
import hudson.model.Run; | ||
import hudson.model.TaskListener; | ||
import java.util.Map; | ||
import org.jenkinsci.plugins.workflow.job.WorkflowRun; | ||
|
||
@Extension | ||
public class DatadogTracerEnvironmentContributor extends EnvironmentContributor { | ||
|
||
@Override | ||
public void buildEnvironmentFor(@NonNull Run run, @NonNull EnvVars envs, @NonNull TaskListener listener) { | ||
if (run instanceof WorkflowRun) { | ||
// Pipelines are handled by org.datadog.jenkins.plugins.datadog.tracer.DatadogTracerStepEnvironmentContributor | ||
return; | ||
} | ||
|
||
Executor executor = run.getExecutor(); | ||
if (executor == null) { | ||
return; | ||
} | ||
|
||
Node node = executor.getOwner().getNode(); | ||
Map<String, String> additionalEnvVars = DatadogTracerConfigurator.INSTANCE.configure(run, node, envs); | ||
envs.putAll(additionalEnvVars); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this with or without TLS? If not, followup feels like it's out of scope but it does appear that there's some sensitive items being sent over the wire (API Key/App key).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The injected APM tracer will always use HTTPS. The plugin itself, when sending job data to Datadog, will use whatever URL is supplied by the user in the config settings, so it depends on whether Datadog allows unencrypted HTTP connections or not. I tried configuring URLs with
http://
protocol, but the server side always seems to negotiate the usage of HTTPS.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding
will use whatever URL is supplied by the user in the config settings
- so we don't necessary enforce this has to go to a datadog domain, and correct me if I'm wrong: this tracer isn't designed to enforce that. It provides the shape of data we expect, but we allow the user to send to any recipient? Because of that, it's a design decision to use HTTP by default, where the recipient ends up negotiating encryption?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
HTTP
constant name might be misleading here, in reality the URL can be eitherhttp
orhttps
, depending on what the user configures.Yes, technically the user can configure the plugin to send the data to any endpoint, not necessarily Datadog. I do not know if this is a valid or expected use case (might be useful if the user wants the data to go through some proxy), but it is technically possible to enter anything into the endpoint URL inputs: the validation logic simply checks that the url string is not empty and that it contains
http
substring.The default values all point to Datadog endpoints, and use
https
protocol. If I change them and specifyhttp
, the remote end (Datadog backend) will still force the usage ofhttps
.As a side note, URL config inputs, validation logic, and different client types were implemented before. In this feature I am using this pre-existing logic to ensure that the injected tracer uses the same config values as the plugin itself.