Skip to content

Commit bdbbf78

Browse files
authored
feat: let user configure timeout and proper manage timeout errors (#43)
* feat: let user configure timeout and proper manage timeout errors * build
1 parent 748e7a8 commit bdbbf78

File tree

7 files changed

+160
-48
lines changed

7 files changed

+160
-48
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ steps:
126126
# Default: false
127127
ignore-timeouts:
128128
129+
# Timeout value in milliseconds for the HTTP client.
130+
# Type: number
131+
# Default: 30000 (30 seconds)
132+
timeout:
133+
129134
# A list of metric objects to send, defined in YAML format
130135
# Type: string
131136
# Default: '[]'

__tests__/main.test.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ describe('unit-tests', () => {
2525
'https://api.datadoghq.com',
2626
'fooBarBaz',
2727
[],
28-
false
28+
false,
29+
30000
2930
)
3031
})
3132

@@ -36,7 +37,8 @@ describe('unit-tests', () => {
3637
'http://example.com',
3738
'fooBarBaz',
3839
[],
39-
false
40+
false,
41+
30000
4042
)
4143
process.env['INPUT_API-URL'] = ''
4244
})
@@ -47,7 +49,8 @@ describe('unit-tests', () => {
4749
'https://http-intake.logs.datadoghq.com',
4850
'fooBarBaz',
4951
[],
50-
false
52+
false,
53+
30000
5154
)
5255
})
5356

@@ -58,7 +61,8 @@ describe('unit-tests', () => {
5861
'http://example.com',
5962
'fooBarBaz',
6063
[],
61-
false
64+
false,
65+
30000
6266
)
6367
process.env['INPUT_LOG-API-URL'] = ''
6468
})
@@ -69,25 +73,29 @@ describe('unit-tests', () => {
6973
'https://api.datadoghq.com',
7074
'fooBarBaz',
7175
[],
72-
false
76+
false,
77+
30000
7378
)
7479
expect(dd.sendEvents).toHaveBeenCalledWith(
7580
'https://api.datadoghq.com',
7681
'fooBarBaz',
7782
[],
78-
false
83+
false,
84+
30000
7985
)
8086
expect(dd.sendServiceChecks).toHaveBeenCalledWith(
8187
'https://api.datadoghq.com',
8288
'fooBarBaz',
8389
[],
84-
false
90+
false,
91+
30000
8592
)
8693
expect(dd.sendLogs).toHaveBeenCalledWith(
8794
'https://http-intake.logs.datadoghq.com',
8895
'fooBarBaz',
8996
[],
90-
false
97+
false,
98+
30000
9199
)
92100
})
93101
})
@@ -148,6 +156,37 @@ describe('end-to-end tests', () => {
148156
env: process.env
149157
}
150158

159+
try {
160+
console.log(cp.execSync(`node ${ip}`, options).toString())
161+
} catch (e) {
162+
console.log(e.output.toString())
163+
throw e
164+
}
165+
})
166+
it('does not fail on timeout errors', () => {
167+
process.env['INPUT_API-KEY'] = process.env['DD_API_KEY'] || ''
168+
if (process.env['INPUT_API-KEY'] === '') {
169+
return
170+
}
171+
process.env['INPUT_IGNORE-TIMEOUTS'] = 'true'
172+
// Set an impossibly low timeout in order to force a client failure
173+
process.env['INPUT_TIMEOUT'] = '10'
174+
175+
process.env['INPUT_METRICS'] = yaml.safeDump([
176+
{
177+
type: 'count',
178+
name: 'test.builds.count',
179+
value: 1.0,
180+
tags: ['foo:bar'],
181+
host: 'example.com'
182+
}
183+
])
184+
185+
const ip = path.join(__dirname, '..', 'lib', 'main.js')
186+
const options: cp.ExecSyncOptions = {
187+
env: process.env
188+
}
189+
151190
try {
152191
console.log(cp.execSync(`node ${ip}`, options).toString())
153192
} catch (e) {

__tests__/timeout.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as cp from 'child_process'
2+
import * as yaml from 'js-yaml'
3+
import * as path from 'path'
4+
import * as process from 'process'
5+
import * as dd from '../src/datadog'
6+
import {run} from '../src/run'
7+
jest.mock('@actions/http-client')
8+
9+
describe('Datadog client timeout tests', () => {
10+
it('should handle timeout errors correctly in sendMetrics', async () => {
11+
// Mock the http client to simulate a timeout
12+
const mockPost = jest.fn().mockImplementation(() => {
13+
return new Promise((_, reject) => {
14+
// Simulate a timeout error
15+
reject(new Error('Request timeout: /api/v1/series'))
16+
})
17+
})
18+
19+
// Mock the HttpClient constructor
20+
require('@actions/http-client').HttpClient.mockImplementation(() => ({
21+
post: mockPost
22+
}))
23+
24+
const metric: dd.Metric = {
25+
type: 'gauge',
26+
name: 'test.metric',
27+
value: 1,
28+
tags: ['test:true'],
29+
host: 'test-host'
30+
}
31+
32+
// Test with ignoreTimeouts = true
33+
await expect(
34+
dd.sendMetrics('http://api.url', 'fake-key', [metric], true, 30000)
35+
).resolves.not.toThrow()
36+
37+
// Test with ignoreTimeouts = false
38+
await expect(
39+
dd.sendMetrics('http://api.url', 'fake-key', [metric], false, 30000)
40+
).rejects.toThrow('Request timeout: /api/v1/series')
41+
})
42+
})

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ inputs:
2727
description: 'Ignore timeout errors and continue execution'
2828
required: false
2929
default: 'false'
30+
timeout:
31+
description: 'Timeout in milliseconds for the HTTP client'
32+
required: false
33+
default: '30'
3034
runs:
3135
using: 'node20'
3236
main: 'dist/index.js'

dist/index.js

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,27 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
3838
exports.sendLogs = exports.sendServiceChecks = exports.sendEvents = exports.sendMetrics = exports.getClient = void 0;
3939
const core = __importStar(__nccwpck_require__(2186));
4040
const httpm = __importStar(__nccwpck_require__(9925));
41-
function getClient(apiKey) {
41+
function getClient(apiKey, timeout) {
4242
return new httpm.HttpClient('dd-http-client', [], {
4343
headers: {
4444
'DD-API-KEY': apiKey,
4545
'Content-Type': 'application/json'
46-
}
46+
},
47+
socketTimeout: timeout
4748
});
4849
}
4950
exports.getClient = getClient;
5051
function isTimeoutError(error) {
51-
return error.message.includes('timeout') || error.message.includes('Timeout');
52+
// force the error into a string so it both works for Error instances and plain strings
53+
const error_msg = `${error}`;
54+
return error_msg.includes('timeout') || error_msg.includes('Timeout');
5255
}
5356
function postMetricsIfAny(http, apiURL, metrics, endpoint, ignoreTimeouts) {
5457
return __awaiter(this, void 0, void 0, function* () {
5558
// POST data
5659
if (metrics.series.length) {
5760
try {
58-
core.debug(`About to send ${metrics.series.length} metrics`);
61+
core.debug(`About to send ${metrics.series.length} metrics to ${apiURL}/api/${endpoint}`);
5962
const res = yield http.post(`${apiURL}/api/${endpoint}`, JSON.stringify(metrics));
6063
if (res.message.statusCode === undefined ||
6164
res.message.statusCode >= 400) {
@@ -72,9 +75,9 @@ function postMetricsIfAny(http, apiURL, metrics, endpoint, ignoreTimeouts) {
7275
}
7376
});
7477
}
75-
function sendMetrics(apiURL, apiKey, metrics, ignoreTimeouts) {
78+
function sendMetrics(apiURL, apiKey, metrics, ignoreTimeouts, timeout) {
7679
return __awaiter(this, void 0, void 0, function* () {
77-
const http = getClient(apiKey);
80+
const http = getClient(apiKey, timeout);
7881
// distributions use a different procotol.
7982
const distributions = { series: Array() };
8083
const otherMetrics = { series: Array() };
@@ -97,11 +100,11 @@ function sendMetrics(apiURL, apiKey, metrics, ignoreTimeouts) {
97100
});
98101
}
99102
exports.sendMetrics = sendMetrics;
100-
function sendEvents(apiURL, apiKey, events, ignoreTimeouts) {
103+
function sendEvents(apiURL, apiKey, events, ignoreTimeouts, timeout) {
101104
return __awaiter(this, void 0, void 0, function* () {
102-
const http = getClient(apiKey);
105+
const http = getClient(apiKey, timeout);
103106
let errors = 0;
104-
core.debug(`About to send ${events.length} events`);
107+
core.debug(`About to send ${events.length} events to ${apiURL}/api/v1/events`);
105108
for (const ev of events) {
106109
try {
107110
const res = yield http.post(`${apiURL}/api/v1/events`, JSON.stringify(ev));
@@ -125,11 +128,11 @@ function sendEvents(apiURL, apiKey, events, ignoreTimeouts) {
125128
});
126129
}
127130
exports.sendEvents = sendEvents;
128-
function sendServiceChecks(apiURL, apiKey, serviceChecks, ignoreTimeouts) {
131+
function sendServiceChecks(apiURL, apiKey, serviceChecks, ignoreTimeouts, timeout) {
129132
return __awaiter(this, void 0, void 0, function* () {
130-
const http = getClient(apiKey);
133+
const http = getClient(apiKey, timeout);
131134
let errors = 0;
132-
core.debug(`About to send ${serviceChecks.length} service checks`);
135+
core.debug(`About to send ${serviceChecks.length} service checks to ${apiURL}/api/v1/check_run`);
133136
for (const sc of serviceChecks) {
134137
try {
135138
const res = yield http.post(`${apiURL}/api/v1/check_run`, JSON.stringify(sc));
@@ -153,11 +156,11 @@ function sendServiceChecks(apiURL, apiKey, serviceChecks, ignoreTimeouts) {
153156
});
154157
}
155158
exports.sendServiceChecks = sendServiceChecks;
156-
function sendLogs(logApiURL, apiKey, logs, ignoreTimeouts) {
159+
function sendLogs(logApiURL, apiKey, logs, ignoreTimeouts, timeout) {
157160
return __awaiter(this, void 0, void 0, function* () {
158-
const http = getClient(apiKey);
161+
const http = getClient(apiKey, timeout);
159162
let errors = 0;
160-
core.debug(`About to send ${logs.length} logs`);
163+
core.debug(`About to send ${logs.length} logs to ${logApiURL}/v1/input`);
161164
for (const log of logs) {
162165
try {
163166
const res = yield http.post(`${logApiURL}/v1/input`, JSON.stringify(log));
@@ -267,16 +270,17 @@ function run() {
267270
return __awaiter(this, void 0, void 0, function* () {
268271
const apiKey = core.getInput('api-key', { required: true });
269272
const apiURL = core.getInput('api-url') || 'https://api.datadoghq.com';
270-
const ignoreTimeouts = core.getInput('ignore-timeouts') === 'false';
273+
const ignoreTimeouts = core.getInput('ignore-timeouts') === 'true';
274+
const timeout = parseInt(core.getInput('timeout')) || 30000;
271275
const metrics = yaml.safeLoad(core.getInput('metrics')) || [];
272-
yield dd.sendMetrics(apiURL, apiKey, metrics, ignoreTimeouts);
276+
yield dd.sendMetrics(apiURL, apiKey, metrics, ignoreTimeouts, timeout);
273277
const events = yaml.safeLoad(core.getInput('events')) || [];
274-
yield dd.sendEvents(apiURL, apiKey, events, ignoreTimeouts);
278+
yield dd.sendEvents(apiURL, apiKey, events, ignoreTimeouts, timeout);
275279
const serviceChecks = yaml.safeLoad(core.getInput('service-checks')) || [];
276-
yield dd.sendServiceChecks(apiURL, apiKey, serviceChecks, ignoreTimeouts);
280+
yield dd.sendServiceChecks(apiURL, apiKey, serviceChecks, ignoreTimeouts, timeout);
277281
const logApiURL = core.getInput('log-api-url') || 'https://http-intake.logs.datadoghq.com';
278282
const logs = yaml.safeLoad(core.getInput('logs')) || [];
279-
yield dd.sendLogs(logApiURL, apiKey, logs, ignoreTimeouts);
283+
yield dd.sendLogs(logApiURL, apiKey, logs, ignoreTimeouts, timeout);
280284
});
281285
}
282286
exports.run = run;

src/datadog.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,20 @@ export interface Log {
3333
service: string
3434
}
3535

36-
export function getClient(apiKey: string): httpm.HttpClient {
36+
export function getClient(apiKey: string, timeout: number): httpm.HttpClient {
3737
return new httpm.HttpClient('dd-http-client', [], {
3838
headers: {
3939
'DD-API-KEY': apiKey,
4040
'Content-Type': 'application/json'
41-
}
41+
},
42+
socketTimeout: timeout
4243
})
4344
}
4445

4546
function isTimeoutError(error: Error): boolean {
46-
return error.message.includes('timeout') || error.message.includes('Timeout')
47+
// force the error into a string so it both works for Error instances and plain strings
48+
const error_msg = `${error}`
49+
return error_msg.includes('timeout') || error_msg.includes('Timeout')
4750
}
4851

4952
async function postMetricsIfAny(
@@ -56,7 +59,9 @@ async function postMetricsIfAny(
5659
// POST data
5760
if (metrics.series.length) {
5861
try {
59-
core.debug(`About to send ${metrics.series.length} metrics`)
62+
core.debug(
63+
`About to send ${metrics.series.length} metrics to ${apiURL}/api/${endpoint}`
64+
)
6065
const res: httpm.HttpClientResponse = await http.post(
6166
`${apiURL}/api/${endpoint}`,
6267
JSON.stringify(metrics)
@@ -86,9 +91,10 @@ export async function sendMetrics(
8691
apiURL: string,
8792
apiKey: string,
8893
metrics: Metric[],
89-
ignoreTimeouts: boolean
94+
ignoreTimeouts: boolean,
95+
timeout: number
9096
): Promise<void> {
91-
const http: httpm.HttpClient = getClient(apiKey)
97+
const http: httpm.HttpClient = getClient(apiKey, timeout)
9298
// distributions use a different procotol.
9399
const distributions = {series: Array()}
94100
const otherMetrics = {series: Array()}
@@ -128,12 +134,13 @@ export async function sendEvents(
128134
apiURL: string,
129135
apiKey: string,
130136
events: Event[],
131-
ignoreTimeouts: boolean
137+
ignoreTimeouts: boolean,
138+
timeout: number
132139
): Promise<void> {
133-
const http: httpm.HttpClient = getClient(apiKey)
140+
const http: httpm.HttpClient = getClient(apiKey, timeout)
134141
let errors = 0
135142

136-
core.debug(`About to send ${events.length} events`)
143+
core.debug(`About to send ${events.length} events to ${apiURL}/api/v1/events`)
137144
for (const ev of events) {
138145
try {
139146
const res: httpm.HttpClientResponse = await http.post(
@@ -167,12 +174,15 @@ export async function sendServiceChecks(
167174
apiURL: string,
168175
apiKey: string,
169176
serviceChecks: ServiceCheck[],
170-
ignoreTimeouts: boolean
177+
ignoreTimeouts: boolean,
178+
timeout: number
171179
): Promise<void> {
172-
const http: httpm.HttpClient = getClient(apiKey)
180+
const http: httpm.HttpClient = getClient(apiKey, timeout)
173181
let errors = 0
174182

175-
core.debug(`About to send ${serviceChecks.length} service checks`)
183+
core.debug(
184+
`About to send ${serviceChecks.length} service checks to ${apiURL}/api/v1/check_run`
185+
)
176186
for (const sc of serviceChecks) {
177187
try {
178188
const res: httpm.HttpClientResponse = await http.post(
@@ -208,12 +218,13 @@ export async function sendLogs(
208218
logApiURL: string,
209219
apiKey: string,
210220
logs: Log[],
211-
ignoreTimeouts: boolean
221+
ignoreTimeouts: boolean,
222+
timeout: number
212223
): Promise<void> {
213-
const http: httpm.HttpClient = getClient(apiKey)
224+
const http: httpm.HttpClient = getClient(apiKey, timeout)
214225
let errors = 0
215226

216-
core.debug(`About to send ${logs.length} logs`)
227+
core.debug(`About to send ${logs.length} logs to ${logApiURL}/v1/input`)
217228
for (const log of logs) {
218229
try {
219230
const res: httpm.HttpClientResponse = await http.post(

0 commit comments

Comments
 (0)