diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99e5a19..3add312 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,12 +12,15 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 22.x cache: npm - name: Install dependencies run: npm install + - name: Run tests + run: npm test + - name: Build ZIP archive run: npm run build diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 895e901..a4b0d2d 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -16,12 +16,15 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 22.x cache: npm - name: Install dependencies run: npm install + - name: Run tests + run: npm test + - name: Build ZIP archive run: npm run build diff --git a/package-lock.json b/package-lock.json index 8388782..d0b358d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,43 +9,47 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@logtail/node": "^0.4.6" + "@logtail/node": "^0.5.8" + }, + "devDependencies": { + "@msgpack/msgpack": "^2.8.0" } }, "node_modules/@logtail/core": { - "version": "0.4.6", + "version": "0.5.8", + "integrity": "sha512-1LxjI46LCXeVRyENwjO4s0a1CWSIqHjYH6w2YVNhQLc5b7mVN0jhzxvmg9mps54aEuCSuK2SVCNmgvuNTDtAug==", "license": "ISC", "dependencies": { - "@logtail/tools": "^0.4.6", - "@logtail/types": "^0.4.6" + "@logtail/tools": "^0.5.8", + "@logtail/types": "^0.5.8", + "serialize-error": "8.1.0" } }, "node_modules/@logtail/node": { - "version": "0.4.6", + "version": "0.5.8", + "integrity": "sha512-9XMxeToOL1QHVNp7IQlPxbRsu3iYUBtXl/SFCyOQQmtnwzS7z+tbiV39DYkXfwkzig6wjwTfLYDQj673Bqplqg==", "license": "ISC", "dependencies": { - "@logtail/core": "^0.4.6", - "@logtail/types": "^0.4.6", + "@logtail/core": "^0.5.8", + "@logtail/types": "^0.5.8", "@msgpack/msgpack": "^2.5.1", - "@types/stack-trace": "^0.0.29", - "cross-fetch": "^3.0.4", - "minimatch": "^3.0.4", - "stack-trace": "^0.0.10" + "@types/stack-trace": "^0.0.33", + "minimatch": "^9.0.5", + "stack-trace": "0.0.10" } }, "node_modules/@logtail/tools": { - "version": "0.4.6", + "version": "0.5.8", + "integrity": "sha512-eAbbHaYSpRoB7Udp301xVUVDeKdgXTP2/s9FOeKX5tPLMM3hgbvuIx50bYJoV/to7bHGYBi36Gdsnj8ElcPXbg==", "license": "ISC", "dependencies": { - "@logtail/types": "^0.4.6" + "@logtail/types": "^0.5.8" } }, "node_modules/@logtail/types": { - "version": "0.4.6", - "license": "ISC", - "dependencies": { - "js": "^0.1.0" - } + "version": "0.5.8", + "integrity": "sha512-CJIijD/2vqK8XwPRZJeUkiiBEhKTviHs5p1WI40WvW21kU18QiQ8PupFJ2uhrItxhwtlkXmRXul5+cNtv3cAFQ==", + "license": "ISC" }, "node_modules/@msgpack/msgpack": { "version": "2.8.0", @@ -55,80 +59,49 @@ } }, "node_modules/@types/stack-trace": { - "version": "0.0.29", + "version": "0.0.33", + "integrity": "sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==", "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/commander": { - "version": "1.1.1", - "dependencies": { - "keypress": "0.1.x" - }, - "engines": { - "node": ">= 0.6.x" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/cross-fetch": { - "version": "3.1.8", + "version": "2.1.2", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "license": "MIT", "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/js": { - "version": "0.1.0", - "dependencies": { - "commander": "~1.1.1" - }, - "bin": { - "js": "bin/js" + "balanced-match": "^1.0.0" } }, - "node_modules/keypress": { - "version": "0.1.0", - "license": "MIT" - }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "9.0.9", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/node-fetch": { - "version": "2.7.0", + "node_modules/serialize-error": { + "version": "8.1.0", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "type-fest": "^0.20.2" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": ">=10" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/stack-trace": { @@ -138,122 +111,89 @@ "node": "*" } }, - "node_modules/tr46": { - "version": "0.0.3", - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "node_modules/type-fest": { + "version": "0.20.2", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } } }, "dependencies": { "@logtail/core": { - "version": "0.4.6", + "version": "0.5.8", + "integrity": "sha512-1LxjI46LCXeVRyENwjO4s0a1CWSIqHjYH6w2YVNhQLc5b7mVN0jhzxvmg9mps54aEuCSuK2SVCNmgvuNTDtAug==", "requires": { - "@logtail/tools": "^0.4.6", - "@logtail/types": "^0.4.6" + "@logtail/tools": "^0.5.8", + "@logtail/types": "^0.5.8", + "serialize-error": "8.1.0" } }, "@logtail/node": { - "version": "0.4.6", + "version": "0.5.8", + "integrity": "sha512-9XMxeToOL1QHVNp7IQlPxbRsu3iYUBtXl/SFCyOQQmtnwzS7z+tbiV39DYkXfwkzig6wjwTfLYDQj673Bqplqg==", "requires": { - "@logtail/core": "^0.4.6", - "@logtail/types": "^0.4.6", + "@logtail/core": "^0.5.8", + "@logtail/types": "^0.5.8", "@msgpack/msgpack": "^2.5.1", - "@types/stack-trace": "^0.0.29", - "cross-fetch": "^3.0.4", - "minimatch": "^3.0.4", - "stack-trace": "^0.0.10" + "@types/stack-trace": "^0.0.33", + "minimatch": "^9.0.5", + "stack-trace": "0.0.10" } }, "@logtail/tools": { - "version": "0.4.6", + "version": "0.5.8", + "integrity": "sha512-eAbbHaYSpRoB7Udp301xVUVDeKdgXTP2/s9FOeKX5tPLMM3hgbvuIx50bYJoV/to7bHGYBi36Gdsnj8ElcPXbg==", "requires": { - "@logtail/types": "^0.4.6" + "@logtail/types": "^0.5.8" } }, "@logtail/types": { - "version": "0.4.6", - "requires": { - "js": "^0.1.0" - } + "version": "0.5.8", + "integrity": "sha512-CJIijD/2vqK8XwPRZJeUkiiBEhKTviHs5p1WI40WvW21kU18QiQ8PupFJ2uhrItxhwtlkXmRXul5+cNtv3cAFQ==" }, "@msgpack/msgpack": { "version": "2.8.0" }, "@types/stack-trace": { - "version": "0.0.29" + "version": "0.0.33", + "integrity": "sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==" }, "balanced-match": { - "version": "1.0.2" + "version": "1.0.2", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "brace-expansion": { - "version": "1.1.11", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "commander": { - "version": "1.1.1", + "version": "2.1.2", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "requires": { - "keypress": "0.1.x" + "balanced-match": "^1.0.0" } }, - "concat-map": { - "version": "0.0.1" - }, - "cross-fetch": { - "version": "3.1.8", - "requires": { - "node-fetch": "^2.6.12" - } - }, - "js": { - "version": "0.1.0", - "requires": { - "commander": "~1.1.1" - } - }, - "keypress": { - "version": "0.1.0" - }, "minimatch": { - "version": "3.1.2", + "version": "9.0.9", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" } }, - "node-fetch": { - "version": "2.7.0", + "serialize-error": { + "version": "8.1.0", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", "requires": { - "whatwg-url": "^5.0.0" + "type-fest": "^0.20.2" } }, "stack-trace": { "version": "0.0.10" }, - "tr46": { - "version": "0.0.3" - }, - "webidl-conversions": { - "version": "3.0.1" - }, - "whatwg-url": { - "version": "5.0.0", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } + "type-fest": { + "version": "0.20.2", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" } } } diff --git a/package.json b/package.json index 006f696..0b8769a 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,14 @@ "url": "https://github.com/logtail/logtail-aws-lambda" }, "dependencies": { - "@logtail/node": "^0.4.6" + "@logtail/node": "^0.5.8" + }, + "devDependencies": { + "@msgpack/msgpack": "^2.8.0" }, "scripts": { - "build": "build/build.sh" + "build": "build/build.sh", + "test": "node --test" }, "license": "MIT" } diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..57c784a --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,127 @@ +import assert from "node:assert/strict" +import { once } from "node:events" +import { createServer } from "node:http" +import test from "node:test" +import { gunzipSync, gzipSync } from "node:zlib" +import { decode } from "@msgpack/msgpack" +import { parseRecords } from "../src/parser.js" + +test("parseRecords adds Lambda context to unknown events", async () => { + const event = { source: "unknown" } + const context = { + functionName: "forward-cloudwatch-logs", + invokedFunctionArn: "arn:aws:lambda:eu-west-1:123456789012:function:forward-cloudwatch-logs", + } + + assert.deepEqual(await parseRecords(event, context), [ + { + message: "Unknown event passed to Better Stack AWS Lambda", + level: "info", + data: { + event, + context: { + logger_lambda_name: context.functionName, + logger_lambda_arn: context.invokedFunctionArn, + }, + }, + }, + ]) +}) + +test("handler sends gzip-compressed MessagePack logs", async t => { + const requests = [] + const server = createServer((request, response) => { + const chunks = [] + + request.on("data", chunk => chunks.push(chunk)) + request.on("end", () => { + requests.push({ + method: request.method, + headers: request.headers, + body: Buffer.concat(chunks), + }) + response.writeHead(202) + response.end() + }) + }) + + server.listen(0, "127.0.0.1") + await once(server, "listening") + + const previousToken = process.env.BETTER_STACK_SOURCE_TOKEN + const previousEntrypoint = process.env.BETTER_STACK_ENTRYPOINT + const { port } = server.address() + + process.env.BETTER_STACK_SOURCE_TOKEN = "source-token" + process.env.BETTER_STACK_ENTRYPOINT = `http://127.0.0.1:${port}` + + t.after(async () => { + if (previousToken === undefined) { + delete process.env.BETTER_STACK_SOURCE_TOKEN + } else { + process.env.BETTER_STACK_SOURCE_TOKEN = previousToken + } + + if (previousEntrypoint === undefined) { + delete process.env.BETTER_STACK_ENTRYPOINT + } else { + process.env.BETTER_STACK_ENTRYPOINT = previousEntrypoint + } + + await new Promise((resolve, reject) => { + server.close(error => error ? reject(error) : resolve()) + }) + }) + + const timestamp = Date.parse("2026-07-15T10:30:00.000Z") + const requestId = "123e4567-e89b-12d3-a456-426614174000" + const cloudWatchPayload = { + messageType: "DATA_MESSAGE", + owner: "123456789012", + logGroup: "/aws/lambda/example", + logStream: "2026/07/15/[$LATEST]abcdef", + logEvents: [ + { + id: "event-id", + timestamp, + message: `[ERROR] 2026-07-15T10:30:00.000Z RequestId: ${requestId} Forwarded message\n`, + }, + ], + } + const event = { + awslogs: { + data: gzipSync(JSON.stringify(cloudWatchPayload)).toString("base64"), + }, + } + const context = { + functionName: "forward-cloudwatch-logs", + invokedFunctionArn: "arn:aws:lambda:eu-west-1:123456789012:function:forward-cloudwatch-logs", + } + + const { handler } = await import("../index.js") + const logs = await handler(event, context) + + assert.equal(logs.length, 1) + assert.equal(requests.length, 1) + + const [request] = requests + assert.equal(request.method, "POST") + assert.equal(request.headers.authorization, "Bearer source-token") + assert.equal(request.headers["content-type"], "application/msgpack") + assert.equal(request.headers["content-encoding"], "gzip") + assert.deepEqual(request.body.subarray(0, 2), Buffer.from([0x1f, 0x8b])) + + const [log] = decode(gunzipSync(request.body)) + assert.equal(log.message, "Forwarded message") + assert.equal(log.level, "error") + assert.equal(log.dt, "2026-07-15T10:30:00.000Z") + assert.deepEqual(log.context, { + owner: cloudWatchPayload.owner, + log_id: "event-id", + log_group: cloudWatchPayload.logGroup, + log_stream: cloudWatchPayload.logStream, + logger_lambda_name: context.functionName, + logger_lambda_arn: context.invokedFunctionArn, + request_id: requestId, + }) +})