Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add assertion and connection error reporters to TCP checks [sc-23082] #1014

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading