Skip to content

Commit

Permalink
Collect and submit jenkins.job.stageduration
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianVeaux committed Jun 18, 2020
1 parent af802ec commit 43740a8
Show file tree
Hide file tree
Showing 5 changed files with 414 additions and 4 deletions.
12 changes: 11 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,23 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-basic-steps</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-durable-task-step</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkinsci.plugins</groupId>
<artifactId>pipeline-model-definition</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
The MIT License
Copyright (c) 2015-Present Datadog, Inc <opensource@datadoghq.com>
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package org.datadog.jenkins.plugins.datadog.listeners;

import hudson.Extension;
import hudson.model.Queue;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.datadog.jenkins.plugins.datadog.DatadogClient;
import org.datadog.jenkins.plugins.datadog.DatadogUtilities;
import org.datadog.jenkins.plugins.datadog.clients.ClientFactory;
import org.datadog.jenkins.plugins.datadog.model.BuildData;
import org.datadog.jenkins.plugins.datadog.util.TagsUtil;
import org.jenkinsci.plugins.workflow.actions.LabelAction;
import org.jenkinsci.plugins.workflow.actions.StageAction;
import org.jenkinsci.plugins.workflow.actions.ThreadNameAction;
import org.jenkinsci.plugins.workflow.actions.TimingAction;
import org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode;
import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode;
import org.jenkinsci.plugins.workflow.flow.GraphListener;
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;

/**
* A GraphListener implementation which computes timing information
* for the various stages in a pipeline.
*/
@Extension
public class DatadogGraphListener implements GraphListener {

private static final Logger logger = Logger.getLogger(DatadogGraphListener.class.getName());

@Override
public void onNewHead(FlowNode flowNode) {
if (!isMonitored(flowNode)) {
return;
}
StepEndNode endNode = (StepEndNode) flowNode;
StepStartNode startNode = endNode.getStartNode();
int stage_depth = 0;
for (BlockStartNode node : startNode.iterateEnclosingBlocks()) {
if (isStageNode(node)) {
stage_depth++;
}
}
DatadogClient client = ClientFactory.getClient();
try {
BuildData buildData = new BuildData(getRun(flowNode), flowNode.getExecution().getOwner().getListener());
String hostname = buildData.getHostname("");
Map<String, Set<String>> tags = buildData.getTags();
TagsUtil.addTagToTags(tags, "stage_name", getStageName(startNode));
TagsUtil.addTagToTags(tags, "stage_depth", String.valueOf(stage_depth));
tags.remove("result"); // Jenkins sometimes consider the build has a result even though it's still running.
client.gauge("jenkins.job.stageduration", getTime(startNode, endNode), hostname, tags);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}

private boolean isMonitored(FlowNode flowNode) {
// Filter the node out if it is not the end of step
// Timing information is only available once the step has completed.
if (!(flowNode instanceof StepEndNode)) {
return false;
}

// Filter the node if the job has been blacklisted from the Datadog plugin configuration.
WorkflowRun run = getRun(flowNode);
if (run == null || !DatadogUtilities.isJobTracked(run.getParent().getFullName())) {
return false;
}

// Filter the node out if it is not the end of a stage.
// The plugin only monitors timing information of stages
return isStageNode(((StepEndNode) flowNode).getStartNode());

// Finally return true as this node is the end of a monitored stage.
}

@CheckForNull
private WorkflowRun getRun(@Nonnull FlowNode flowNode) {
Queue.Executable exec;
try {
exec = flowNode.getExecution().getOwner().getExecutable();
} catch (IOException e) {
// Ignore the error, that step cannot be monitored.
return null;
}

// TODO: WorkflowRun or Run ?
if (exec instanceof WorkflowRun) {
return (WorkflowRun) exec;
}
return null;
}

boolean isStageNode(BlockStartNode flowNode) {
if (flowNode == null) {
return false;
}
if (flowNode.getAction(StageAction.class) != null) {
// Legacy style stage block without a body
// https://groups.google.com/g/jenkinsci-users/c/MIVk-44cUcA
return true;
}
if (flowNode.getAction(ThreadNameAction.class) != null) {
// TODO comment
return false;
}
return flowNode.getAction(LabelAction.class) != null;
}

String getStageName(@Nonnull StepStartNode flowNode) {
ThreadNameAction threadNameAction = flowNode.getAction(ThreadNameAction.class);
if (threadNameAction != null) {
return threadNameAction.getThreadName();
}
return flowNode.getDisplayName();
}

long getTime(FlowNode startNode, FlowNode endNode) {
TimingAction startTime = startNode.getAction(TimingAction.class);
TimingAction endTime = endNode.getAction(TimingAction.class);

if (startTime != null && endTime != null) {
return endTime.getStartTime() - startTime.getStartTime();
}
return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,11 @@ of this software and associated documentation files (the "Software"), to deal
package org.datadog.jenkins.plugins.datadog.clients;

import hudson.util.Secret;
import net.sf.json.JSON;
import net.sf.json.JSONObject;
import org.datadog.jenkins.plugins.datadog.DatadogClient;
import org.datadog.jenkins.plugins.datadog.DatadogEvent;
import org.junit.Assert;

import javax.servlet.ServletException;
import java.io.IOException;
import java.util.*;

public class DatadogClientStub implements DatadogClient {
Expand Down Expand Up @@ -153,6 +150,18 @@ public boolean assertMetric(String name, double value, String hostname, String[]
return false;
}

public boolean assertMetric(String name, String hostname, String[] tags) {
DatadogMetric m = new DatadogMetric(name, 0, hostname, Arrays.asList(tags));
Optional<DatadogMetric> match = this.metrics.stream().filter(t -> t.same(m)).findFirst();
if(match.isPresent()){
this.metrics.remove(match.get());
return true;
}
Assert.fail("metric { " + m.toString() + " does not exist (ignoring value). " +
"metrics: {" + this.metrics.toString() + " }");
return false;
}

public boolean assertServiceCheck(String name, int code, String hostname, String[] tags) {
DatadogMetric m = new DatadogMetric(name, code, hostname, Arrays.asList(tags));
if (this.serviceChecks.contains(m)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package org.datadog.jenkins.plugins.datadog.listeners;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import hudson.model.labels.LabelAtom;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.datadog.jenkins.plugins.datadog.DatadogUtilities;
import org.datadog.jenkins.plugins.datadog.clients.ClientFactory;
import org.datadog.jenkins.plugins.datadog.clients.DatadogClientStub;
import org.jenkinsci.plugins.workflow.actions.LabelAction;
import org.jenkinsci.plugins.workflow.actions.ThreadNameAction;
import org.jenkinsci.plugins.workflow.actions.TimingAction;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode;
import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;

public class DatadogGraphListenerTest {

@ClassRule
public static JenkinsRule jenkinsRule = new JenkinsRule();
private DatadogGraphListener listener;
private DatadogClientStub clientStub;

@Before
public void beforeEach() {
listener = new DatadogGraphListener();
clientStub = new DatadogClientStub();
ClientFactory.setTestClient(clientStub);
}

private StepStartNode makeMonitorableStartNode(String label) {
StepStartNode startNode = mock(StepStartNode.class);
when(startNode.getAction(LabelAction.class)).thenReturn(new LabelAction(label));
when(startNode.getDisplayName()).thenReturn(label);
return startNode;
}

@Test
public void testNewNode() throws IOException {
StepStartNode startNode = makeMonitorableStartNode("low");
StepEndNode endNode = mock(StepEndNode.class);
when(endNode.getStartNode()).thenReturn(startNode);

long startTime = 10L, endTime = 12345L;

TimingAction startAction = mock(TimingAction.class);
when(startAction.getStartTime()).thenReturn(startTime);
TimingAction endAction = mock(TimingAction.class);
when(endAction.getStartTime()).thenReturn(endTime);

when(startNode.getAction(TimingAction.class)).thenReturn(startAction);
when(endNode.getAction(TimingAction.class)).thenReturn(endAction);

List<BlockStartNode> enclosingBlocks = new ArrayList<>();
enclosingBlocks.add(makeMonitorableStartNode("medium"));
enclosingBlocks.add(makeMonitorableStartNode("high"));
when(startNode.iterateEnclosingBlocks()).thenReturn(enclosingBlocks);

WorkflowJob job = jenkinsRule.jenkins.createProject(WorkflowJob.class, "pipeline");
WorkflowRun run = new WorkflowRun(job);

FlowExecutionOwner flowExecutionOwner = mock(FlowExecutionOwner.class);
when(flowExecutionOwner.getExecutable()).thenReturn(run);
FlowExecution flowExecution = mock(FlowExecution.class);
when(flowExecution.getOwner()).thenReturn(flowExecutionOwner);
when(endNode.getExecution()).thenReturn(flowExecution);

listener.onNewHead(endNode);
String hostname = DatadogUtilities.getHostname(null);
String[] expectedTags = new String[]{
"jenkins_url:" + DatadogUtilities.getJenkinsUrl(),
"user_id:anonymous",
"stage_name:low",
"job:pipeline",
"stage_depth:2"
};
clientStub.assertMetric("jenkins.job.stageduration", endTime - startTime, hostname, expectedTags);
}

@Test
public void testIntegration() throws Exception {
jenkinsRule.createOnlineSlave(new LabelAtom("windows"));
WorkflowJob job = jenkinsRule.jenkins.createProject(WorkflowJob.class, "pipelineIntegration");
String definition = IOUtils.toString(
this.getClass().getResourceAsStream("testPipelineDefinition.txt"),
"UTF-8"
);
job.setDefinition(new CpsFlowDefinition(definition, true));
WorkflowRun run = job.scheduleBuild2(0).get();
BufferedReader br = new BufferedReader(run.getLogReader());
String s;
while ((s = br.readLine()) != null) {
System.out.println(s);
}
br.close();
String hostname = DatadogUtilities.getHostname(null);
String[] baseTags = new String[]{
"jenkins_url:" + DatadogUtilities.getJenkinsUrl(),
"user_id:anonymous",
"job:pipelineIntegration"
};
String[] depths = new String[]{ "2", "2", "2", "1", "1", "0", "0" };
String[] stageNames = new String[]{ "Windows-1", "Windows-2", "Windows-3", "Test On Windows", "Test On Linux", "Parallel tests",
"Pre-setup" };
for (int i = 0; i < depths.length; i++) {
String[] expectedTags = Arrays.copyOf(baseTags, baseTags.length + 2);
expectedTags[expectedTags.length - 2] = "stage_depth:" + depths[i];
expectedTags[expectedTags.length - 1] = "stage_name:" + stageNames[i];
clientStub.assertMetric("jenkins.job.stageduration", hostname, expectedTags);
}

}

@Test
public void isStageNodeTest() {
Assert.assertFalse(listener.isStageNode(null));
BlockStartNode node = mock(BlockStartNode.class);
Assert.assertFalse(listener.isStageNode(node));

when(node.getAction(LabelAction.class)).thenReturn(mock(LabelAction.class));
Assert.assertTrue(listener.isStageNode(node));

when(node.getAction(ThreadNameAction.class)).thenReturn(mock(ThreadNameAction.class));
Assert.assertFalse(listener.isStageNode(node));
}

@Test
public void getStageNameTest() {
String stageName = "Hello world";
StepStartNode node = mock(StepStartNode.class);
when(node.getDisplayName()).thenReturn(stageName);

// Regular stage with no thread
when(node.getAction(ThreadNameAction.class)).thenReturn(null);
Assert.assertEquals(listener.getStageName(node), stageName);

// Parallel stage
String stageNameThread = "Hello thread";
ThreadNameAction threadNameAction = mock(ThreadNameAction.class);
when(threadNameAction.getThreadName()).thenReturn(stageNameThread);
when(node.getAction(ThreadNameAction.class)).thenReturn(threadNameAction);
Assert.assertEquals(listener.getStageName(node), stageNameThread);
}


@Test
public void getTimeTest() {
long startTime = 10L, endTime = 12345L;

TimingAction startAction = mock(TimingAction.class);
when(startAction.getStartTime()).thenReturn(startTime);
TimingAction endAction = mock(TimingAction.class);
when(endAction.getStartTime()).thenReturn(endTime);

StepStartNode startNode = mock(StepStartNode.class);
when(startNode.getAction(TimingAction.class)).thenReturn(startAction);
StepEndNode endNode = mock(StepEndNode.class);
when(endNode.getAction(TimingAction.class)).thenReturn(endAction);

Assert.assertEquals(endTime - startTime, listener.getTime(startNode, endNode));
}

}

0 comments on commit 43740a8

Please sign in to comment.