forked from DataDog/datadog-ci
-
Notifications
You must be signed in to change notification settings - Fork 0
/
trace.ts
235 lines (209 loc) · 7.29 KB
/
trace.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import {spawn} from 'child_process'
import crypto from 'crypto'
import os from 'os'
import chalk from 'chalk'
import {Command, Option} from 'clipanion'
import {retryRequest} from '../../helpers/retry'
import {parseTags} from '../../helpers/tags'
import {apiConstructor} from './api'
import {APIHelper, CIRCLECI, JENKINS, Payload, Provider, SUPPORTED_PROVIDERS} from './interfaces'
// We use 127 as exit code for invalid commands since that is what *sh terminals return
const BAD_COMMAND_EXIT_CODE = 127
export class TraceCommand extends Command {
public static paths = [['trace']]
public static usage = Command.Usage({
category: 'CI Visibility',
description: 'Trace a command with a custom span and report it to Datadog.',
details: `
This command wraps another command, which it will launch, and report a custom span to Datadog.\n
See README for details.
`,
examples: [
[
'Trace a command with name "Say Hello" and report to Datadog',
'datadog-ci trace --name "Say Hello" -- echo "Hello World"',
],
[
'Trace a command with name "Say Hello" and a extra tags and report to Datadog',
'datadog-ci trace --name "Say Hello" --tags key1:value1 --tags key2:value2 -- echo "Hello World"',
],
[
'Trace a command and report to the datadoghq.eu site',
'DD_SITE=datadoghq.eu datadog-ci trace -- echo "Hello World"',
],
],
})
private command = Option.Rest({required: 1})
private name = Option.String('--name')
private noFail = Option.Boolean('--no-fail')
private tags = Option.Array('--tags')
private config = {
apiKey: process.env.DATADOG_API_KEY || process.env.DD_API_KEY,
envVarTags: process.env.DD_TAGS,
}
public async execute() {
if (!this.command || !this.command.length) {
this.context.stderr.write('Missing command to run\n')
return 1
}
const [command, ...args] = this.command
const id = crypto.randomBytes(5).toString('hex')
const startTime = new Date().toISOString()
const childProcess = spawn(command, args, {
env: {...process.env, DD_CUSTOM_PARENT_ID: id},
stdio: ['inherit', 'inherit', 'pipe'],
})
const chunks: Buffer[] = []
childProcess.stderr.pipe(this.context.stderr)
const stderrCatcher: Promise<string> = new Promise((resolve, reject) => {
childProcess.stderr.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
childProcess.stderr.on('error', (err) => reject(err))
childProcess.stderr.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
})
const [status, signal] = await new Promise<[number, NodeJS.Signals]>((resolve, reject) => {
childProcess.on('error', (error: Error) => {
reject(error)
})
childProcess.on('close', (exitStatus: number, exitSignal: NodeJS.Signals) => {
resolve([exitStatus, exitSignal])
})
})
const stderr: string = await stderrCatcher
const endTime = new Date().toISOString()
const exitCode: number = status ?? this.signalToNumber(signal) ?? BAD_COMMAND_EXIT_CODE
const [ciEnvVars, provider] = this.getCIEnvVars()
if (provider) {
const commandStr = this.command.join(' ')
const envVarTags = this.config.envVarTags ? parseTags(this.config.envVarTags.split(',')) : {}
const cliTags = this.tags ? parseTags(this.tags) : {}
await this.reportCustomSpan(
{
command: commandStr,
custom: {
id,
parent_id: process.env.DD_CUSTOM_PARENT_ID,
},
data: ciEnvVars,
end_time: endTime,
error_message: stderr,
exit_code: exitCode,
is_error: exitCode !== 0,
name: this.name ?? commandStr,
start_time: startTime,
tags: {
...cliTags,
...envVarTags,
},
},
provider
)
}
return exitCode
}
public getCIEnvVars(): [Record<string, string>, Provider?] {
if (process.env.CIRCLECI) {
return [
this.getEnvironmentVars([
'CIRCLE_BRANCH',
'CIRCLE_BUILD_NUM',
'CIRCLE_BUILD_URL',
'CIRCLE_JOB',
'CIRCLE_NODE_INDEX',
'CIRCLE_NODE_TOTAL',
'CIRCLE_PROJECT_REPONAME',
'CIRCLE_PULL_REQUEST',
'CIRCLE_REPOSITORY_URL',
'CIRCLE_SHA1',
'CIRCLE_TAG',
'CIRCLE_WORKFLOW_ID',
]),
CIRCLECI,
]
}
if (process.env.JENKINS_HOME) {
if (!process.env.DD_CUSTOM_TRACE_ID) {
this.context.stdout.write(
`${chalk.yellow.bold(
'[WARNING]'
)} Your Jenkins instance does not seem to be instrumented with the Datadog plugin.\n`
)
this.context.stdout.write(
'Please follow the instructions at https://docs.datadoghq.com/continuous_integration/setup_pipelines/jenkins/\n'
)
return [{}]
}
return [
this.getEnvironmentVars([
'BUILD_ID',
'BUILD_NUMBER',
'BUILD_TAG',
'BUILD_URL',
'DD_CUSTOM_TRACE_ID',
'EXECUTOR_NUMBER',
'GIT_AUTHOR_EMAIL',
'GIT_AUTHOR_NAME',
'GIT_BRANCH',
'GIT_COMMIT',
'GIT_COMMITTER_EMAIL',
'GIT_COMMITTER_NAME',
'GIT_URL',
'GIT_URL_1',
'JENKINS_URL',
'JOB_BASE_NAME',
'JOB_NAME',
'JOB_URL',
'NODE_NAME',
'NODE_LABELS',
'WORKSPACE',
]),
JENKINS,
]
}
const errorMsg = `Cannot detect any supported CI Provider. This command only works if run as part of your CI. Supported providers: ${SUPPORTED_PROVIDERS}.`
if (this.noFail) {
this.context.stdout.write(
`${chalk.yellow.bold('[WARNING]')} ${errorMsg} Not failing since the --no-fail options was used.\n`
)
return [{}]
} else {
throw new Error(errorMsg)
}
}
private getApiHelper(): APIHelper {
if (!this.config.apiKey) {
this.context.stdout.write(
`Neither ${chalk.red.bold('DATADOG_API_KEY')} nor ${chalk.red.bold('DD_API_KEY')} is in your environment.\n`
)
throw new Error('API key is missing')
}
return apiConstructor(this.getBaseIntakeUrl(), this.config.apiKey)
}
private getBaseIntakeUrl() {
const site = process.env.DATADOG_SITE || process.env.DD_SITE || 'datadoghq.com'
return `https://webhook-intake.${site}`
}
private getEnvironmentVars(keys: string[]): Record<string, string> {
return keys.filter((key) => key in process.env).reduce((accum, key) => ({...accum, [key]: process.env[key]!}), {})
}
private async reportCustomSpan(payload: Payload, provider: Provider) {
const api = this.getApiHelper()
try {
await retryRequest(() => api.reportCustomSpan(payload, provider), {
onRetry: (e, attempt) => {
this.context.stderr.write(
chalk.yellow(`[attempt ${attempt}] Could not report custom span. Retrying...: ${e.message}\n`)
)
},
retries: 5,
})
} catch (error) {
this.context.stderr.write(chalk.red(`Failed to report custom span: ${error.message}\n`))
}
}
private signalToNumber(signal: NodeJS.Signals | null): number | undefined {
if (!signal) {
return undefined
}
return os.constants.signals[signal] + 128
}
}