Skip to content

Commit

Permalink
feat: add assertion and connection error reporters to TCP checks [sc-…
Browse files Browse the repository at this point in the history
…23082] (#1014)

* feat: add fixtures for various failing TCP checks - no tests yet

* feat: add assertion and connection error reporters to TCP checks
  • Loading branch information
sorccu authored Jan 29, 2025
1 parent 3e31cf8 commit df80080
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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',
},
})
161 changes: 161 additions & 0 deletions packages/cli/src/reporters/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -132,6 +152,7 @@ const assertionSources: any = {
HEADERS: 'headers',
TEXT_BODY: 'text body',
RESPONSE_TIME: 'response time',
RESPONSE_DATA: 'response data',
}

const assertionComparisons: any = {
Expand Down Expand Up @@ -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 ?? '<None>'})`,
].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)
Expand Down

0 comments on commit df80080

Please sign in to comment.