From 6508d3ba1abc591a096b29c8b86818ec1be55b30 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Thu, 30 Jan 2025 04:57:39 +0900 Subject: [PATCH 1/2] feat: add fixtures for various failing TCP checks - no tests yet --- .../tcp-check-failures/checkly.config.ts | 6 ++ .../fixtures/tcp-check-failures/tcp.check.ts | 72 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 packages/cli/e2e/__tests__/fixtures/tcp-check-failures/checkly.config.ts create mode 100644 packages/cli/e2e/__tests__/fixtures/tcp-check-failures/tcp.check.ts diff --git a/packages/cli/e2e/__tests__/fixtures/tcp-check-failures/checkly.config.ts b/packages/cli/e2e/__tests__/fixtures/tcp-check-failures/checkly.config.ts new file mode 100644 index 00000000..c06c3664 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/tcp-check-failures/checkly.config.ts @@ -0,0 +1,6 @@ +const config = { + projectName: 'TCP Check Failures', + logicalId: process.env.PROJECT_LOGICAL_ID, + repoUrl: 'https://github.com/checkly/checkly-cli', +} +export default config diff --git a/packages/cli/e2e/__tests__/fixtures/tcp-check-failures/tcp.check.ts b/packages/cli/e2e/__tests__/fixtures/tcp-check-failures/tcp.check.ts new file mode 100644 index 00000000..b1882c3b --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/tcp-check-failures/tcp.check.ts @@ -0,0 +1,72 @@ +/* eslint-disable no-new */ +import { TcpCheck, TcpAssertionBuilder } from 'checkly/constructs' + +new TcpCheck('tcp-check-dns-failure-ipv4', { + name: 'TCP check with DNS lookup failure (IPv4)', + activated: true, + request: { + hostname: 'does-not-exist.checklyhq.com', + port: 443, + }, +}) + +new TcpCheck('tcp-check-dns-failure-ipv6', { + name: 'TCP check with DNS lookup failure (IPv6)', + activated: true, + request: { + hostname: 'does-not-exist.checklyhq.com', + port: 443, + ipFamily: 'IPv6', + }, +}) + +new TcpCheck('tcp-check-connection-refused', { + name: 'TCP check for connection that gets refused', + activated: true, + request: { + hostname: '127.0.0.1', + port: 12345, + }, +}) + +new TcpCheck('tcp-check-connection-refused-2', { + name: 'TCP check for connection that gets refused #2', + activated: true, + request: { + hostname: '0.0.0.0', + port: 12345, + }, +}) + +new TcpCheck('tcp-check-timed-out', { + name: 'TCP check for connection that times out', + activated: true, + request: { + hostname: 'api.checklyhq.com', + port: 9999, + }, +}) + +new TcpCheck('tcp-check-failing-assertions', { + name: 'TCP check with failing assertions', + activated: true, + request: { + hostname: 'api.checklyhq.com', + port: 80, + data: 'GET / HTTP/1.1\r\nHost: api.checklyhq.com\r\nConnection: close\r\n\r\n', + assertions: [ + TcpAssertionBuilder.responseData().contains('NEVER_PRESENT'), + TcpAssertionBuilder.responseTime().lessThan(1), + ], + }, +}) + +new TcpCheck('tcp-check-wrong-ip-family', { + name: 'TCP check with wrong IP family', + activated: true, + request: { + hostname: 'ipv4.google.com', + port: 80, + ipFamily: 'IPv6', + }, +}) From 1c9e281dafeee488d4a99a51b352e8e54707e253 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Thu, 30 Jan 2025 05:01:26 +0900 Subject: [PATCH 2/2] feat: add assertion and connection error reporters to TCP checks --- packages/cli/src/reporters/util.ts | 161 +++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/packages/cli/src/reporters/util.ts b/packages/cli/src/reporters/util.ts index 988480c3..c2a64569 100644 --- a/packages/cli/src/reporters/util.ts +++ b/packages/cli/src/reporters/util.ts @@ -104,6 +104,26 @@ export function formatCheckResult (checkResult: any) { ]) } } + } if (checkResult.checkType === 'TCP') { + if (checkResult.checkRunData?.requestError) { + result.push([ + formatSectionTitle('Request Error'), + checkResult.checkRunData.requestError, + ]) + } else { + if (checkResult.checkRunData?.response?.error) { + result.push([ + formatSectionTitle('Connection Error'), + formatConnectionError(checkResult.checkRunData?.response?.error), + ]) + } + if (checkResult.checkRunData?.assertions?.length) { + result.push([ + formatSectionTitle('Assertions'), + formatAssertions(checkResult.checkRunData.assertions), + ]) + } + } } if (checkResult.logs?.length) { result.push([ @@ -132,6 +152,7 @@ const assertionSources: any = { HEADERS: 'headers', TEXT_BODY: 'text body', RESPONSE_TIME: 'response time', + RESPONSE_DATA: 'response data', } const assertionComparisons: any = { @@ -216,6 +237,146 @@ function formatHttpResponse (response: any) { ].filter(Boolean).join('\n') } +// IPv4 lookup for a non-existing hostname: +// +// { +// "code": "ENOTFOUND", +// "syscall": "queryA", +// "hostname": "does-not-exist.checklyhq.com" +// } +// +// IPv6 lookup for a non-existing hostname: +// +// { +// "code": "ENOTFOUND", +// "syscall": "queryAaaa", +// "hostname": "does-not-exist.checklyhq.com" +// } +interface DNSLookupFailureError { + code: 'ENOTFOUND' + syscall: string + hostname: string +} + +function isDNSLookupFailureError (error: any): error is DNSLookupFailureError { + return error.code === 'ENOTFOUND' && + typeof error.syscall === 'string' && + typeof error.hostname === 'string' +} + +// Connection attempt to a port that isn't open: +// +// { +// "errno": -111, +// "code": "ECONNREFUSED", +// "syscall": "connect", +// "address": "127.0.0.1", +// "port": 22 +// } +// +interface ConnectionRefusedError { + code: 'ECONNREFUSED' + errno?: number + syscall: string + address: string + port: number +} + +function isConnectionRefusedError (error: any): error is ConnectionRefusedError { + return error.code === 'ECONNREFUSED' && + typeof error.syscall === 'string' && + typeof error.address === 'string' && + typeof error.port === 'number' && + typeof (error.errno ?? 0) === 'number' +} + +// Connection kept open after data exchange and it timed out: +// +// { +// "code": "SOCKET_TIMEOUT", +// "address": "api.checklyhq.com", +// "port": 9999 +// } +interface SocketTimeoutError { + code: 'SOCKET_TIMEOUT' + address: string + port: number +} + +function isSocketTimeoutError (error: any): error is SocketTimeoutError { + return error.code === 'SOCKET_TIMEOUT' && + typeof error.address === 'string' && + typeof error.port === 'number' +} + +// Invalid IP address (e.g. IPv4-only hostname when IPFamily is IPv6) +// +// { +// "code": "ERR_INVALID_IP_ADDRESS", +// } +interface InvalidIPAddressError { + code: 'ERR_INVALID_IP_ADDRESS' +} + +function isInvalidIPAddressError (error: any): error is InvalidIPAddressError { + return error.code === 'ERR_INVALID_IP_ADDRESS' +} + +function formatConnectionError (error: any) { + if (isDNSLookupFailureError(error)) { + const message = [ + logSymbols.error, + `DNS lookup for "${error.hostname}" failed`, + `(syscall: ${error.syscall})`, + ].join(' ') + return chalk.red(message) + } + + if (isConnectionRefusedError(error)) { + const message = [ + logSymbols.error, + `Connection to "${error.address}:${error.port}" was refused`, + `(syscall: ${error.syscall}, errno: ${error.errno ?? ''})`, + ].join(' ') + return chalk.red(message) + } + + if (isSocketTimeoutError(error)) { + const message = [ + logSymbols.error, + `Connection to "${error.address}:${error.port}" timed out (perhaps connection was never closed)`, + ].join(' ') + return chalk.red(message) + } + + if (isInvalidIPAddressError(error)) { + const message = [ + logSymbols.error, + 'Invalid IP address (perhaps hostname and IP family do not match)', + ].join(' ') + return chalk.red(message) + } + + // Some other error we don't have detection for. + if (error.code !== undefined) { + const { code, ...extra } = error + const detailsString = JSON.stringify(extra) + const message = [ + logSymbols.error, + `${code} (details: ${detailsString})`, + ].join(' ') + return chalk.red(message) + } + + // If we don't even have a code, give up and output the whole thing. + const detailsString = JSON.stringify(error) + const message = [ + logSymbols.error, + `Error (details: ${detailsString})`, + ].join(' ') + return chalk.red(message) +} + function formatLogs (logs: Array<{ level: string, msg: string, time: number }>) { return logs.flatMap(({ level, msg, time }) => { const timestamp = DateTime.fromMillis(time).toLocaleString(DateTime.TIME_24_WITH_SECONDS)