seq-logging
Advanced tools
Comparing version 1.1.2 to 2.0.0
@@ -19,3 +19,2 @@ { | ||
"html-webpack-plugin": "^5.3", | ||
"node-polyfill-webpack-plugin": "^1.1", | ||
"webpack": "^5.38", | ||
@@ -22,0 +21,0 @@ "webpack-cli": "^4.7", |
const path = require('path'); | ||
const HtmlWebpackPlugin = require('html-webpack-plugin'); | ||
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); | ||
module.exports = { | ||
entry: './src/main.js', | ||
resolve: { | ||
fallback: { | ||
"buffer": false | ||
} | ||
}, | ||
plugins: [ | ||
@@ -12,3 +16,2 @@ new HtmlWebpackPlugin({ | ||
}), | ||
new NodePolyfillPlugin(), | ||
], | ||
@@ -15,0 +18,0 @@ module: { |
{ | ||
"name": "seq-logging", | ||
"version": "1.1.2", | ||
"version": "2.0.0", | ||
"description": "Sends structured log events to the Seq HTTP ingestion API", | ||
@@ -25,3 +25,2 @@ "keywords": [ | ||
"mocha": "^9.2.2", | ||
"node-polyfill-webpack-plugin": "^1.1.4", | ||
"nyc": "^15.1.0", | ||
@@ -32,3 +31,7 @@ "simple-mock": "^0.8.0", | ||
"uuid": "^8.3.2" | ||
}, | ||
"dependencies": { | ||
"abort-controller": "^3.0.0", | ||
"node-fetch": "^2.6.9" | ||
} | ||
} |
# Seq Logging for JavaScript ![Build](https://github.com/datalust/seq-logging/workflows/Test/badge.svg) ![Publish](https://github.com/datalust/seq-logging/workflows/Publish/badge.svg) [![NPM](https://img.shields.io/npm/v/seq-logging.svg)](https://www.npmjs.com/package/seq-logging) | ||
> This library makes it easy to support Seq from Node.js logging libraries, including [Pino](https://github.com/pinojs/pino) via [`pino-seq`](https://github.com/datalust/pino-seq), [Bunyan](https://github.com/trentm/node-bunyan) via [`bunyan-seq`](https://github.com/continuousit/bunyan-seq), and [Ts.ED logger](https://logger.tsed.io) via [@tsed/logger-seq](https://logger.tsed.io/appenders/seq.html). It is not expected that applications will interact directly with this package. | ||
> This library makes it easy to support Seq from Node.js logging libraries, including [Winston](https://github.com/winstonjs/winston) via [winston-seq](https://github.com/datalust/winston-seq), [Pino](https://github.com/pinojs/pino) via [`pino-seq`](https://github.com/datalust/pino-seq), [Bunyan](https://github.com/trentm/node-bunyan) via [`bunyan-seq`](https://github.com/continuousit/bunyan-seq), and [Ts.ED logger](https://logger.tsed.io) via [@tsed/logger-seq](https://logger.tsed.io/appenders/seq.html). It is not expected that applications will interact directly with this package. | ||
@@ -5,0 +5,0 @@ ### Usage |
"use strict"; | ||
let http = require('http'); | ||
let https = require('https'); | ||
let url = require('url'); | ||
const SafeGlobalBlob = typeof Blob !== 'undefined' ? Blob : require('buffer').Blob; | ||
const safeGlobalFetch = typeof fetch !== 'undefined' ? fetch : require('node-fetch'); | ||
const SafeGlobalAbortController = typeof AbortController !== 'undefined' ? AbortController : require('abort-controller'); | ||
const HEADER = '{"Events":['; | ||
const FOOTER = "]}"; | ||
const HEADER_FOOTER_BYTES = Buffer.byteLength(HEADER, 'utf8') + Buffer.byteLength(FOOTER, 'utf8'); | ||
const HEADER_FOOTER_BYTES = (new SafeGlobalBlob([HEADER])).size + (new SafeGlobalBlob([FOOTER])).size; | ||
class SeqLogger { | ||
@@ -30,3 +29,3 @@ constructor(config) { | ||
} | ||
this._endpoint = url.parse(serverUrl + 'api/events/raw'); | ||
this._endpoint = serverUrl + 'api/events/raw'; | ||
this._apiKey = cfg.apiKey || dflt.apiKey; | ||
@@ -47,8 +46,2 @@ this._maxBatchingTime = cfg.maxBatchingTime || dflt.maxBatchingTime; | ||
this._lastRemoteConfig = null; | ||
this._httpModule = this._endpoint.protocol === "https:" ? https : http | ||
this._httpAgent = new this._httpModule.Agent({ | ||
keepAlive: true, | ||
maxTotalSockets: 25, // recommendation from https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-configuring-maxsockets.html | ||
}); | ||
} | ||
@@ -65,33 +58,2 @@ | ||
/** | ||
* * A browser only function that queues events for sending using the navigator.sendBeacon() API. | ||
* * This may work in an unload or pagehide event handler when a normal flush() would not. | ||
* * Events over 63K in length are discarded (with a warning sent in its place) and the total size batch will be no more than 63K in length. | ||
* @returns {boolean} | ||
*/ | ||
flushToBeacon () { | ||
if (this._queue.length === 0) { | ||
return false; | ||
} | ||
if (typeof navigator === 'undefined' || !navigator.sendBeacon || typeof Blob === 'undefined') { | ||
return false; | ||
} | ||
const currentBatchSizeLimit = this._batchSizeLimit; | ||
const currentEventSizeLimit = this._eventSizeLimit; | ||
this._batchSizeLimit = Math.min(63 * 1024, this._batchSizeLimit); | ||
this._eventSizeLimit = Math.min(63 * 1024, this._eventSizeLimit); | ||
const dequeued = this._dequeBatch(); | ||
this._batchSizeLimit = currentBatchSizeLimit; | ||
this._eventSizeLimit = currentEventSizeLimit; | ||
const { dataParts, options, beaconUrl, size } = this._prepForBeacon(dequeued); | ||
const data = new Blob(dataParts, options); | ||
return navigator.sendBeacon(beaconUrl, data); | ||
} | ||
/** | ||
* Flush then destroy connections, close the logger, destroying timers and other resources. | ||
@@ -107,5 +69,3 @@ * @returns {Promise<void>} | ||
this._clearTimer(); | ||
return this.flush().then(() => { | ||
this._httpAgent.destroy(); | ||
}); | ||
return this.flush(); | ||
} | ||
@@ -257,3 +217,3 @@ | ||
} | ||
var jsonLen = Buffer.byteLength(json, 'utf8'); | ||
var jsonLen = new SafeGlobalBlob([json]).size; | ||
if (jsonLen > this._eventSizeLimit) { | ||
@@ -263,3 +223,3 @@ this._onError("[seq] Event body is larger than " + this._eventSizeLimit + " bytes: " + json); | ||
json = JSON.stringify(next); | ||
jsonLen = Buffer.byteLength(json, 'utf8'); | ||
jsonLen = new SafeGlobalBlob([json]).size; | ||
} | ||
@@ -285,3 +245,3 @@ | ||
const networkErrors = ['ECONNRESET', 'ENOTFOUND', 'ESOCKETTIMEDOUT', 'ETIMEDOUT', 'ECONNREFUSED', 'EHOSTUNREACH', 'EPIPE', 'EAI_AGAIN', 'EBUSY']; | ||
return networkErrors.includes(res) || 500 <= res.statusCode && res.statusCode < 600; | ||
return networkErrors.includes(res) || 500 <= res.status && res.status < 600; | ||
} | ||
@@ -294,74 +254,40 @@ | ||
const sendRequest = (batch, bytes) => { | ||
const controller = new SafeGlobalAbortController(); | ||
attempts++; | ||
let req = this._httpModule.request({ | ||
host: this._endpoint.hostname, | ||
port: this._endpoint.port, | ||
path: this._endpoint.path, | ||
protocol: this._endpoint.protocol, | ||
agent: this._httpAgent, | ||
headers: { | ||
"Content-Type": "application/json", | ||
"X-Seq-ApiKey": this._apiKey ? this._apiKey : null, | ||
"Content-Length": bytes, | ||
}, | ||
method: "POST", | ||
timeout: this._requestTimeout | ||
}); | ||
const timerId = setTimeout(() => { | ||
controller.abort(); | ||
if (attempts > this._maxRetries) { | ||
reject('HTTP log shipping failed, reached timeout (' + this._requestTimeout + ' ms)'); | ||
} else { | ||
setTimeout(() => sendRequest(batch, bytes), this._retryDelay); | ||
} | ||
}, this._requestTimeout); | ||
req.on("socket", (socket) => { | ||
if (socket.listeners("timeout").length == 0) { | ||
socket.on("timeout", () => { | ||
req.destroy(); | ||
if (attempts > this._maxRetries) { | ||
return reject('HTTP log shipping failed, reached timeout (' + this._requestTimeout + ' ms)') | ||
} else { | ||
return setTimeout(() => sendRequest(batch, bytes), this._retryDelay); | ||
} | ||
}); | ||
safeGlobalFetch(this._endpoint, { | ||
keepalive: true, | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
"X-Seq-ApiKey": this._apiKey ? this._apiKey : null, | ||
"Content-Length": bytes, | ||
}, | ||
body: `${HEADER}${batch.join(',')}${FOOTER}`, | ||
signal: controller.signal, | ||
}) | ||
.then((res) => { | ||
clearTimeout(timerId); | ||
let httpErr = null; | ||
if (res.status !== 200 && res.status !== 201) { | ||
httpErr = 'HTTP log shipping failed: ' + res.status; | ||
if (this._httpOrNetworkError(res) && attempts < this._maxRetries) { | ||
return setTimeout(() => sendRequest(batch, bytes), this._retryDelay); | ||
} | ||
return reject(httpErr); | ||
} | ||
}); | ||
req.on('response', res => { | ||
var httpErr = null; | ||
if (res.statusCode !== 200 && res.statusCode !== 201) { | ||
httpErr = 'HTTP log shipping failed: ' + res.statusCode; | ||
} | ||
res.on('data', (buffer) => { | ||
let dataRaw = buffer.toString(); | ||
if (this._onRemoteConfigChange && this._lastRemoteConfig !== dataRaw) { | ||
this._lastRemoteConfig = dataRaw; | ||
this._onRemoteConfigChange(JSON.parse(dataRaw)); | ||
} | ||
}); | ||
res.on('error', e => { | ||
return reject(e); | ||
}); | ||
res.on('end', () => { | ||
if (httpErr !== null) { | ||
if (this._httpOrNetworkError(res) && attempts < this._maxRetries) { | ||
return setTimeout(() => sendRequest(batch, bytes), this._retryDelay); | ||
} | ||
return reject(httpErr); | ||
} else { | ||
return resolve(true); | ||
} | ||
}); | ||
}); | ||
req.on('error', e => { | ||
return reject(e); | ||
}); | ||
req.write(HEADER); | ||
var delim = ""; | ||
for (var b = 0; b < batch.length; b++) { | ||
req.write(delim); | ||
delim = ","; | ||
req.write(batch[b]); | ||
} | ||
req.write(FOOTER); | ||
req.end(); | ||
return resolve(true); | ||
}) | ||
.catch((err) => { | ||
clearTimeout(timerId); | ||
reject(err); | ||
}) | ||
} | ||
@@ -372,20 +298,2 @@ | ||
} | ||
_prepForBeacon (dequeued) { | ||
const { batch, bytes } = dequeued; | ||
const dataParts = [HEADER, batch.join(','), FOOTER]; | ||
// CORS-safelisted for the Content-Type request header | ||
const options = { type: 'text/plain' }; | ||
const endpointWithKey = Object.assign({}, this._endpoint, { query: { 'apiKey': this._apiKey } }); | ||
return { | ||
dataParts, | ||
options, | ||
beaconUrl: url.format(endpointWithKey), | ||
size: bytes, | ||
}; | ||
} | ||
} | ||
@@ -392,0 +300,0 @@ |
"use strict"; | ||
let assert = require('assert'); | ||
let simple = require('simple-mock'); | ||
const http = require("http"); | ||
@@ -13,6 +12,3 @@ let SeqLogger = require('../seq_logger'); | ||
let logger = new SeqLogger(); | ||
assert.strictEqual(logger._endpoint.hostname, 'localhost'); | ||
assert.strictEqual(logger._endpoint.port, '5341'); | ||
assert.strictEqual(logger._endpoint.protocol, 'http:'); | ||
assert.strictEqual(logger._endpoint.path, '/api/events/raw'); | ||
assert.strictEqual(logger._endpoint, 'http://localhost:5341/api/events/raw'); | ||
assert.strictEqual(logger._apiKey, null); | ||
@@ -25,6 +21,3 @@ assert.strictEqual(logger._maxRetries, 5); | ||
let logger = new SeqLogger({ serverUrl: 'https://my-seq/prd', apiKey: '12345', maxRetries: 10, retryDelay: 10000 }); | ||
assert.strictEqual(logger._endpoint.hostname, 'my-seq'); | ||
assert.strictEqual(logger._endpoint.port, null); | ||
assert.strictEqual(logger._endpoint.protocol, 'https:'); | ||
assert.strictEqual(logger._endpoint.path, '/prd/api/events/raw'); | ||
assert.strictEqual(logger._endpoint, 'https://my-seq/prd/api/events/raw'); | ||
assert.strictEqual(logger._apiKey, '12345'); | ||
@@ -35,6 +28,2 @@ assert.strictEqual(logger._maxRetries, 10); | ||
it('correctly formats slashed paths', () => { | ||
let logger = new SeqLogger({serverUrl: 'https://my-seq/prd/'}); | ||
assert.strictEqual(logger._endpoint.path, '/prd/api/events/raw'); | ||
}); | ||
}); | ||
@@ -114,57 +103,5 @@ | ||
describe('flushToBeacon()', function() { | ||
const sendBeacon = simple.stub().returnWith(true); | ||
const MockBlob = function MockBlob(blobParts, options) { | ||
this.size = blobParts.join('').length; | ||
this.type = (options && options.type) || '' | ||
} | ||
beforeEach(function() { | ||
simple.mock(global, 'navigator', {sendBeacon}); | ||
simple.mock(global, 'Blob', MockBlob); | ||
}); | ||
it('return false with no events', function() { | ||
let logger = new SeqLogger(); | ||
const result = logger.flushToBeacon(); | ||
assert.strictEqual(result, false); | ||
}); | ||
it('formats url to include api key', function() { | ||
let logger = new SeqLogger({serverUrl: 'https://my-seq/prd', apiKey: '12345'}); | ||
let event = makeTestEvent(); | ||
logger.emit(event); | ||
logger._clearTimer(); | ||
const {dataParts, options, beaconUrl, size} = logger._prepForBeacon({batch: [], bytes: 11}); | ||
assert.strictEqual(beaconUrl, 'https://my-seq/prd/api/events/raw?apiKey=12345'); | ||
}); | ||
it('queues beacon', function() { | ||
let logger = new SeqLogger({serverUrl: 'https://my-seq/prd', apiKey: '12345'}); | ||
let event = makeTestEvent(); | ||
logger.emit(event); | ||
logger._clearTimer(); | ||
const result = logger.flushToBeacon(); | ||
assert(result); | ||
assert.strictEqual(sendBeacon.callCount, 1); | ||
assert.strictEqual(sendBeacon.lastCall.args[0], 'https://my-seq/prd/api/events/raw?apiKey=12345'); | ||
assert.strictEqual(sendBeacon.lastCall.args[1].type, 'text/plain'); | ||
assert.strictEqual(sendBeacon.lastCall.args[1].size, 168); | ||
}); | ||
it('does handle event properties with circular structures', () => { | ||
let logger = new SeqLogger({serverUrl: 'https://my-seq/prd', apiKey: '12345'}); | ||
const event = makeCircularTestEvent(); | ||
logger.emit(event); | ||
logger.flushToBeacon(); | ||
}); | ||
afterEach(function() { | ||
sendBeacon.reset(); | ||
simple.restore(); | ||
}); | ||
}); | ||
describe("_post()", function () { | ||
it("retries 5 times after 5xx response from seq server", async () => { | ||
const mockSeq = new MockSeq(); | ||
const mockSeq = new MockSeq(3000); | ||
try { | ||
@@ -186,6 +123,6 @@ await mockSeq.ready; | ||
it("does not retry on 4xx responses", async () => { | ||
const mockSeq = new MockSeq(); | ||
const mockSeq = new MockSeq(3001); | ||
try { | ||
await mockSeq.ready; | ||
const logger = new SeqLogger({ serverUrl: 'http://localhost:3000', maxBatchingTime: 1, retryDelay: 100 }); | ||
const logger = new SeqLogger({ serverUrl: 'http://localhost:3001', maxBatchingTime: 1, retryDelay: 100 }); | ||
const event = makeTestEvent(); | ||
@@ -204,6 +141,6 @@ | ||
it("retries the amount of times set in configuration", async () => { | ||
const mockSeq = new MockSeq(); | ||
const mockSeq = new MockSeq(3002); | ||
try { | ||
await mockSeq.ready; | ||
const logger = new SeqLogger({ serverUrl: 'http://localhost:3000', maxBatchingTime: 1, retryDelay: 100, maxRetries: 7 }); | ||
const logger = new SeqLogger({ serverUrl: 'http://localhost:3002', maxBatchingTime: 1, retryDelay: 100, maxRetries: 7 }); | ||
const event = makeTestEvent(); | ||
@@ -226,3 +163,3 @@ | ||
class MockSeq extends http.Server { | ||
constructor() { | ||
constructor(port) { | ||
super((_, res) => { | ||
@@ -236,3 +173,3 @@ res.statusCode = this.status; | ||
this.ready = new Promise((resolve, reject) => { | ||
this.listen(3000, "localhost") | ||
this.listen(port, "localhost") | ||
.once('listening', resolve) | ||
@@ -254,13 +191,1 @@ .once('error', reject); | ||
function makeCircularTestEvent() { | ||
const a = {}; | ||
a.a = a; | ||
return { | ||
level: "Error", | ||
timestamp: new Date(), | ||
messageTemplate: 'Circular structure issue!', | ||
exception: "Some error at some file on some line", | ||
properties: { a } | ||
}; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
6
46634
2
808
+ Addedabort-controller@^3.0.0
+ Addednode-fetch@^2.6.9
+ Addedabort-controller@3.0.0(transitive)
+ Addedevent-target-shim@5.0.1(transitive)
+ Addednode-fetch@2.7.0(transitive)
+ Addedtr46@0.0.3(transitive)
+ Addedwebidl-conversions@3.0.1(transitive)
+ Addedwhatwg-url@5.0.0(transitive)