Skip to content
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
65 changes: 2 additions & 63 deletions packages/logging/lib/GhostLogger.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ class GhostLogger {
* mode: Is used to print short or long log.
* level: The level defines the default level of all transports except of stderr.
* logBody: Disable or enable if the body of a request should be logged to the target stream.
* transports: An array of comma separated transports (e.g. stdout, stderr, geld, loggly, file)
* transports: An array of comma separated transports (e.g. stdout, stderr, gelf, file)
* rotation: Enable or disable file rotation.
* path: Path where to store log files.
* filename: Optional filename template for log files. Supports {env} and {domain} placeholders.
* If not provided, defaults to {domain}_{env} format.
* loggly: Loggly transport configuration.
* elasticsearch: Elasticsearch transport configuration
* gelf: Gelf transport configuration.
* http: HTTP transport configuration
Expand All @@ -48,7 +47,6 @@ class GhostLogger {
this.mode = process.env.MODE || options.mode || 'short';
this.path = options.path || process.cwd();
this.filename = options.filename || '{domain}_{env}';
this.loggly = options.loggly || {};
this.elasticsearch = options.elasticsearch || {};
this.gelf = options.gelf || {};
this.http = options.http || {};
Expand Down Expand Up @@ -179,35 +177,6 @@ class GhostLogger {
};
}

/**
* @description Setup loggly.
*/
setLogglyStream() {
const Bunyan2Loggly = require('bunyan-loggly');

const logglyStream = new Bunyan2Loggly({
token: this.loggly.token,
subdomain: this.loggly.subdomain,
tags: this.loggly.tags,
});

this.streams.loggly = {
name: 'loggly',
match: this.loggly.match,
log: bunyan.createLogger({
name: this.name,
streams: [
{
type: 'raw',
stream: logglyStream,
level: 'error',
},
],
serializers: this.serializers,
}),
};
}

/**
* @description Setup ElasticSearch.
*/
Expand Down Expand Up @@ -599,37 +568,7 @@ class GhostLogger {
return;
}

/**
* @NOTE
* Only `loggly` offers the `match` option.
* And currently `loggly` is by default configured to only send errors (not configureable).
* e.g. level info would get ignored.
*
* @NOTE
* The `match` feature is not completed. We hardcode checking if the level/type is `error` for now.
* Otherwise each `level:info` would has to run through the matching logic.
*
* @NOTE
* Matching a string in the whole req/res object massively slows down the process, because it's a sync
* operation.
*
* If we want to extend the feature, we can only offer matching certain keys e.g. status code, headers.
* If we want to extend the feature, we have to do proper performance testing.
*
* `jsonStringifySafe` can match a string in an object, which has circular dependencies.
* https://github.com/moll/json-stringify-safe
*/
if (logger.match && type === 'error') {
if (
new RegExp(logger.match).test(
jsonStringifySafe(modifiedArguments[0].err || null).replace(/"/g, ''),
)
) {
logger.log[type](...modifiedArguments);
}
} else {
logger.log[type](...modifiedArguments);
}
logger.log[type](...modifiedArguments);
});
}

Expand Down
1 change: 0 additions & 1 deletion packages/logging/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"@tryghost/pretty-stream": "workspace:*",
"@tryghost/root-utils": "workspace:*",
"bunyan": "1.8.15",
"bunyan-loggly": "2.0.1",
"fs-extra": "11.3.5",
"gelf-stream": "1.1.1",
"json-stringify-safe": "5.0.1",
Expand Down
218 changes: 10 additions & 208 deletions packages/logging/test/logging.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const includes = require('lodash/includes');
const errors = require('@tryghost/errors');
const sinon = require('sinon');
const assert = require('assert/strict');
const Bunyan2Loggly = require('bunyan-loggly');
const GelfStream = require('gelf-stream').GelfStream;
const ElasticSearch = require('@tryghost/elasticsearch').BunyanStream;
const HttpStream = require('@tryghost/http-stream');
Expand Down Expand Up @@ -251,201 +250,6 @@ describe('Logging', function () {
assert.equal(GelfStream.prototype._write.called, false);
});

it('loggly does only stream certain errors', async function () {
await new Promise((resolve) => {
sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) {
assert.notEqual(data.err, null);
resolve();
});

var ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
match: 'level:critical',
},
});

ghostLogger.error(new errors.InternalServerError());
assert.equal(Bunyan2Loggly.prototype.write.called, true);
});
});

it('loggly does not stream non-critical errors when matching critical', function () {
sandbox.spy(Bunyan2Loggly.prototype, 'write');

var ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
match: 'level:critical',
},
});

ghostLogger.error(new errors.NotFoundError());
assert.equal(Bunyan2Loggly.prototype.write.called, false);
});

it('loggly does not stream errors that do not match regex', function () {
sandbox.spy(Bunyan2Loggly.prototype, 'write');

var ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
match: '^((?!statusCode:4\\d{2}).)*$',
},
});

ghostLogger.error(new errors.NotFoundError());
assert.equal(Bunyan2Loggly.prototype.write.called, false);
});

it('loggly does not stream errors when not nested correctly', function () {
sandbox.spy(Bunyan2Loggly.prototype, 'write');

var ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
match: '^((?!statusCode:4\\d{2}).)*$',
},
});

ghostLogger.error(new errors.NoPermissionError());
assert.equal(Bunyan2Loggly.prototype.write.called, false);
});

it('loggly does stream errors that match regex', function () {
sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function () {});

var ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
match: '^((?!statusCode:4\\d{2}).)*$',
},
});

ghostLogger.error(new errors.InternalServerError());
assert.equal(Bunyan2Loggly.prototype.write.called, true);
});

it('loggly does stream errors that match normal level', async function () {
await new Promise((resolve) => {
sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) {
assert.notEqual(data.err, null);
resolve();
});

var ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
match: 'level:normal',
},
});

ghostLogger.error(new errors.NotFoundError());
assert.equal(Bunyan2Loggly.prototype.write.called, true);
});
});

it('loggly match can evaluate with null err payload', function () {
sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function () {});

const ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
match: '^null$',
},
});

ghostLogger.error('plain error message');
assert.equal(Bunyan2Loggly.prototype.write.called, true);
});

it('loggly does stream errors that match an one of multiple match statements', async function () {
await new Promise((resolve) => {
sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) {
assert.notEqual(data.err, null);
resolve();
});

var ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
match: 'level:critical|statusCode:404',
},
});

ghostLogger.error(new errors.NotFoundError());
assert.equal(Bunyan2Loggly.prototype.write.called, true);
});
});

it('loggly does stream errors that match status code: full example', async function () {
await new Promise((resolve) => {
sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) {
assert.notEqual(data.err, null);
assert.notEqual(data.req, null);
assert.notEqual(data.res, null);
resolve();
});

var ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
match: 'statusCode:404',
},
});

ghostLogger.error({
err: new errors.NotFoundError(),
req: {
body: {
password: '12345678',
data: { attributes: { pin: '1234', test: 'ja' } },
},
},
res: { getHeaders: () => ({}) },
});
assert.equal(Bunyan2Loggly.prototype.write.called, true);
});
});

it('loggly does only stream certain errors: match is not defined -> log everything', async function () {
await new Promise((resolve) => {
sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) {
assert.notEqual(data.err, null);
resolve();
});

var ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
},
});

ghostLogger.error(new errors.NotFoundError());
assert.equal(Bunyan2Loggly.prototype.write.called, true);
});
});

it('elasticsearch should make a stream', function () {
const es = new ElasticSearch(
{
Expand Down Expand Up @@ -751,7 +555,7 @@ describe('Logging', function () {
await new Promise((resolve) => {
const err = new errors.NotFoundError();

sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) {
sandbox.stub(HttpStream.prototype, 'write').callsFake(function (data) {
assert.notEqual(data.err, null);
assert.equal(data.err.id, err.id);
assert.equal(data.err.domain, 'localhost');
Expand All @@ -769,23 +573,22 @@ describe('Logging', function () {
});

const ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
transports: ['http'],
http: {
url: 'http://localhost',
},
});
ghostLogger.error({
err,
});

assert.equal(Bunyan2Loggly.prototype.write.called, true);
assert.equal(HttpStream.prototype.write.called, true);
});
});

it('stringifies meta properties', async function () {
await new Promise((resolve) => {
sandbox.stub(Bunyan2Loggly.prototype, 'write').callsFake(function (data) {
sandbox.stub(HttpStream.prototype, 'write').callsFake(function (data) {
assert.notEqual(data.err, null);
assert.equal(data.err.context, '{"a":"b"}');
assert.equal(data.err.errorDetails, '{"c":"d"}');
Expand All @@ -794,10 +597,9 @@ describe('Logging', function () {
});

const ghostLogger = new GhostLogger({
transports: ['loggly'],
loggly: {
token: 'invalid',
subdomain: 'invalid',
transports: ['http'],
http: {
url: 'http://localhost',
},
});
ghostLogger.error({
Expand All @@ -814,7 +616,7 @@ describe('Logging', function () {
}),
});

assert.equal(Bunyan2Loggly.prototype.write.called, true);
assert.equal(HttpStream.prototype.write.called, true);
});
});

Expand Down
Loading
Loading