Skip to content
Merged
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
149 changes: 62 additions & 87 deletions seq_logger.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
"use strict";
var NodeBlob, NodeFetch, NodeAbortController
try {
NodeBlob = require('buffer').Blob
} catch (error) {
console.log('missing buffer')
}
try {
NodeFetch = require('node-fetch')
} catch (error) {
console.log('missing node-fetch')
}
try {
NodeAbortController = require('abort-controller')
} catch (error) {
console.log('missing abort-controller')
}

let http = require('http');
let https = require('https');
let url = require('url');
const SafeGlobalBlob = typeof Blob !== 'undefined' ? Blob : NodeBlob;
const safeGlobalFetch = typeof fetch !== 'undefined' ? fetch : NodeFetch;
const SafeGlobalAbortController = typeof AbortController !== 'undefined' ? AbortController : NodeAbortController;

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch can be polyfilled by referencing node-fetch@2 and then something like:

const fetchApi = typeof(fetch) === 'undefined' ? require('node-fetch') : fetch;

then it works in node 16

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly different approach but done!

class SeqLogger {
constructor(config) {
Expand All @@ -27,7 +42,7 @@ class SeqLogger {
if (!serverUrl.endsWith('/')) {
serverUrl += '/';
}
this._endpoint = url.parse(serverUrl + 'api/events/raw');
this._endpoint = serverUrl + 'api/events/raw';
this._apiKey = cfg.apiKey || dflt.apiKey;
this._maxBatchingTime = cfg.maxBatchingTime || dflt.maxBatchingTime;
this._eventSizeLimit = cfg.eventSizeLimit || dflt.eventSizeLimit;
Expand All @@ -43,12 +58,6 @@ class SeqLogger {
this._activeShipper = null;
this._onRemoteConfigChange = cfg.onRemoteConfigChange || null;
this._lastRemoteConfig = null;

this._httpModule = this._endpoint.protocol === "https:" ? https : http
this._httpAgent = new this._httpModule.Agent({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reference to _httpAgent on line 96 causes a crash

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

keepAlive: true,
maxTotalSockets: 25, // recommendation from https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-configuring-maxsockets.html
});
}

/**
Expand Down Expand Up @@ -101,9 +110,7 @@ class SeqLogger {

this._closed = true;
this._clearTimer();
return this.flush().then(() => {
this._httpAgent.destroy();
});
return this.flush().then(() => {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the .then(() => {}) doesn't do anything you can just:

return this.flush();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct

}

/**
Expand Down Expand Up @@ -250,12 +257,12 @@ class SeqLogger {
messageTemplate: "[seq] Circular structure found"
});
}
var jsonLen = Buffer.byteLength(json, 'utf8');
var jsonLen = new SafeGlobalBlob([json]).size;
if (jsonLen > this._eventSizeLimit) {
this._onError("[seq] Event body is larger than " + this._eventSizeLimit + " bytes: " + json);
this._queue[i] = next = this._eventTooLargeErrorEvent(next);
json = JSON.stringify(next);
jsonLen = Buffer.byteLength(json, 'utf8');
jsonLen = new SafeGlobalBlob([json]).size;
}

// Always try to send a batch of at least one event, even if the batch size is
Expand Down Expand Up @@ -284,78 +291,47 @@ class SeqLogger {

return new Promise((resolve, reject) => {
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
});

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);
}
});
}
});

req.on('response', res => {
var httpErr = null;
if (res.statusCode !== 200 && res.statusCode !== 201) {
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);

safeGlobalFetch(this._endpoint, {
keepalive: true,
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Seq-ApiKey": this._apiKey ? this._apiKey : null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my testing this is not working properly. X-Seq-ApiKey: null causes this error 'Payload from [...] specified invalid API key ·····'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you test it?
Do you guys using your test-suits?

"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.statusCode;
}

res.on('data', (buffer) => {
let dataRaw = buffer.toString();

if (this._onRemoteConfigChange && this._lastRemoteConfig !== dataRaw) {
this._lastRemoteConfig = dataRaw;
this._onRemoteConfigChange(JSON.parse(dataRaw));
if (httpErr !== null) {
if (this._httpOrNetworkError(res) && attempts < this._maxRetries) {
return setTimeout(() => sendRequest(batch, bytes), this._retryDelay);
}
});

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 reject(httpErr);
} else {
return resolve(true);
}
})
.catch((err) => {
clearTimeout(timerId);
reject(err);
})
}

return sendRequest(batch, bytes);
Expand All @@ -369,8 +345,7 @@ class SeqLogger {

// CORS-safelisted for the Content-Type request header
const options = { type: 'text/plain' };

const endpointWithKey = Object.assign({}, this._endpoint, { query: { 'apiKey': this._apiKey } });
const endpointWithKey = Object.assign({}, new URL(this._endpoint), { query: { 'apiKey': this._apiKey } });

return {
dataParts,
Expand Down