forked from DataDog/datadog-ci
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmetric.ts
146 lines (123 loc) · 4.49 KB
/
metric.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
import chalk from 'chalk'
import {Command} from 'clipanion'
import {getCIEnv, PROVIDER_TO_DISPLAY_NAME} from '../../helpers/ci'
import {retryRequest} from '../../helpers/retry'
import {getApiHostForSite, getRequestBuilder} from '../../helpers/utils'
export const parseMetrics = (metrics: string[]) =>
metrics.reduce((acc, keyValue) => {
if (!keyValue.includes(':')) {
throw new Error(`invalid metrics key value pair "${keyValue}"`)
}
const [key, value] = keyValue.split(':', 2)
const floatVal = parseFloat(value)
if (isNaN(floatVal)) {
throw new Error('value is not numeric')
}
return {
...acc,
[key]: floatVal,
}
}, {})
export class MetricCommand extends Command {
public static usage = Command.Usage({
description: 'Add metrics to a CI Pipeline trace pipeline or job span in Datadog.',
details: `
This command when run from a supported CI provider sends an arbitrary set of key:value
numeric tags to Datadog to include in the CI Visibility traces.
`,
examples: [
['Add a binary size to the current pipeline', 'datadog-ci metric --level pipeline --tags binary.size:500'],
['Tag the current CI job with a command runtime', 'datadog-ci metric --level job --tags command.runtime:67.1'],
],
})
private config = {
apiKey: process.env.DATADOG_API_KEY || process.env.DD_API_KEY,
}
private level?: string
private metrics?: string[]
private noFail?: boolean
public async execute() {
if (this.level !== 'pipeline' && this.level !== 'job') {
this.context.stderr.write(`${chalk.red.bold('[ERROR]')} Level must be one of [pipeline, job]\n`)
return 1
}
if (!this.metrics || this.metrics.length === 0) {
this.context.stderr.write(`${chalk.red.bold('[ERROR]')} --metrics is required\n`)
return 1
}
try {
const metrics = parseMetrics(this.metrics)
const {provider, ciEnv} = getCIEnv()
// For GitHub and Buddy only the pipeline level is supported as there is no way to identify the job from the runner.
if ((provider === 'github' || provider === 'buddy') && this.level === 'job') {
this.context.stderr.write(
`${chalk.red.bold('[ERROR]')} Cannot use level "job" for ${PROVIDER_TO_DISPLAY_NAME[provider]}.`
)
return 1
}
const exitStatus = await this.sendMetrics(ciEnv, this.level === 'pipeline' ? 0 : 1, provider, metrics)
if (exitStatus !== 0 && this.noFail) {
this.context.stderr.write(
`${chalk.yellow.bold('[WARNING]')} sending metrics failed but continuing due to --no-fail\n`
)
return 0
} else if (exitStatus === 0) {
this.context.stdout.write('Metrics sent\n')
}
return exitStatus
} catch (error) {
this.context.stderr.write(`${chalk.red.bold('[ERROR]')} ${error.message}\n`)
return 1
}
}
private async sendMetrics(
ciEnv: Record<string, string>,
level: number,
provider: string,
metrics: Record<string, number>
): Promise<number> {
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')
}
const site = process.env.DATADOG_SITE || process.env.DD_SITE || 'datadoghq.com'
const baseAPIURL = `https://${getApiHostForSite(site)}`
const request = getRequestBuilder({baseUrl: baseAPIURL, apiKey: this.config.apiKey})
const doRequest = () =>
request({
data: {
data: {
attributes: {
ci_env: ciEnv,
ci_level: level,
metrics,
provider,
},
type: 'ci_custom_metric',
},
},
method: 'post',
url: 'api/v2/ci/pipeline/metrics',
})
try {
await retryRequest(doRequest, {
onRetry: (e, attempt) => {
this.context.stderr.write(
chalk.yellow(`[attempt ${attempt}] Could not send metrics. Retrying...: ${e.message}\n`)
)
},
retries: 5,
})
} catch (error) {
this.context.stderr.write(`${chalk.red.bold('[ERROR]')} Could not send metrics: ${error.message}\n`)
return 1
}
return 0
}
}
MetricCommand.addPath('metric')
MetricCommand.addOption('noFail', Command.Boolean('--no-fail'))
MetricCommand.addOption('metrics', Command.Array('--metrics'))
MetricCommand.addOption('level', Command.String('--level'))