Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
feat(logging): enable sending logs to OpenTelemetry (#1097)
Browse files Browse the repository at this point in the history
  • Loading branch information
10xLaCroixDrinker authored Sep 22, 2023
1 parent 9bd6f41 commit a0b8cb6
Show file tree
Hide file tree
Showing 29 changed files with 901 additions and 120 deletions.
2 changes: 1 addition & 1 deletion __tests__/integration/helpers/moduleMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

const fs = require('fs-extra');
const { resolve, join } = require('path');
const util = require('util');
const util = require('node:util');
const childProcess = require('child_process');

const promisifiedExec = util.promisify(childProcess.exec);
Expand Down
108 changes: 80 additions & 28 deletions __tests__/server/config/env/runTime.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,57 @@
* permissions and limitations under the License.
*/

expect.extend({
toValidateURL(input) {
let passNegativeCase;
let passPositiveCase;
try {
input('//example.com/path');
input('/path');
passNegativeCase = false;
} catch (e) {
passNegativeCase = true;
}
try {
input('https://example.com/path');
passPositiveCase = true;
} catch (e) {
passPositiveCase = false;
}
return {
pass: passNegativeCase && passPositiveCase,
message: () => `${this.utils.matcherHint('toValidateURL', undefined, '')
}\n\nExpected function to validate input is a fetchable URL in Node`,
};
},
toValidatePositiveInteger(input) {
let passNegativeCase;
let passPositiveCase;
try {
input('string that evaluates to NaN');
input('0');
input('-6');
input('4.3');
passNegativeCase = false;
} catch (e) {
passNegativeCase = true;
}
try {
input('1');
input('3001');
input(undefined);
passPositiveCase = true;
} catch (e) {
passPositiveCase = false;
}
return {
pass: passNegativeCase && passPositiveCase,
message: () => `${this.utils.matcherHint('toValidatePositiveInteger', undefined, '')
}\n\nExpected function to validate input is a positive integer`,
};
},
});

jest.mock('ip', () => ({
address: () => 'localhost',
}));
Expand Down Expand Up @@ -86,25 +137,6 @@ describe('runTime', () => {
jest.resetAllMocks();
});

function nodeUrl(entry) {
return () => {
expect(() => entry.validate('https://example.com/path')).not.toThrow();
expect(() => entry.validate('/path')).toThrow();
};
}

function positiveInteger(entry) {
return () => {
expect(() => entry.validate('1')).not.toThrow();
expect(() => entry.validate('3001')).not.toThrow();
expect(() => entry.validate(undefined)).not.toThrow();
expect(() => entry.validate('string that evaluates to NaN')).toThrow();
expect(() => entry.validate('0')).toThrow();
expect(() => entry.validate('-6')).toThrow();
expect(() => entry.validate('4.3')).toThrow();
};
}

it('has a name on every entry', () => {
const runTime = require('../../../../src/server/config/env/runTime').default;
runTime.forEach((entry) => {
Expand Down Expand Up @@ -281,8 +313,9 @@ describe('runTime', () => {
expect(holocronModuleMapPath.defaultValue()).not.toBeDefined();
});

// eslint-disable-next-line jest/expect-expect -- assertion is in nodeUrl
it('ensures node can reach the URL', nodeUrl(holocronModuleMapPath));
it('ensures node can reach the URL', () => {
expect(holocronModuleMapPath.validate).toValidateURL();
});

it('should use port numbers specified via HTTP_ONE_APP_DEV_CDN_PORT', () => {
process.env.NODE_ENV = 'development';
Expand All @@ -300,8 +333,9 @@ describe('runTime', () => {
expect(holocronServerMaxModulesRetry.defaultValue).toBe(undefined);
});

// eslint-disable-next-line jest/expect-expect -- assertion is in positiveInteger
it('validates the value as a positive integer', positiveInteger(holocronServerMaxModulesRetry));
it('validates the value as a positive integer', () => {
expect(holocronServerMaxModulesRetry.validate).toValidatePositiveInteger();
});
});

describe('HOLOCRON_SERVER_MAX_SIM_MODULES_FETCH', () => {
Expand All @@ -313,11 +347,9 @@ describe('runTime', () => {
expect(holocronServerMaxSimModulesFetch.defaultValue).toBe(undefined);
});

// eslint-disable-next-line jest/expect-expect -- assertion is in positiveInteger
it(
'validates the value as a positive integer',
positiveInteger(holocronServerMaxSimModulesFetch)
);
it('validates the value as a positive integer', () => {
expect(holocronServerMaxSimModulesFetch.validate).toValidatePositiveInteger();
});
});

describe('ONE_CLIENT_REPORTING_URL', () => {
Expand Down Expand Up @@ -497,4 +529,24 @@ describe('runTime', () => {
expect(() => postRequestMaxPayload.validate('20kb')).not.toThrow();
});
});

describe('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', () => {
const otelLogCollectorUrl = getEnvVarConfig('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT');

it('ensures node can reach the URL', () => {
expect(otelLogCollectorUrl.validate).toValidateURL();
});

it('is not required', () => {
expect(() => otelLogCollectorUrl.validate()).not.toThrow();
});
});

describe('OTEL_SERVICE_NAME', () => {
const otelServiceName = getEnvVarConfig('OTEL_SERVICE_NAME');

it('should have a default value of "One App"', () => {
expect(otelServiceName.defaultValue).toBe('One App');
});
});
});
2 changes: 1 addition & 1 deletion __tests__/server/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* permissions and limitations under the License.
*/

import util from 'util';
import util from 'node:util';
import fs from 'fs';
import path from 'path';

Expand Down
2 changes: 1 addition & 1 deletion __tests__/server/plugins/reactHtml/index.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* permissions and limitations under the License.
*/

import util from 'util';
import util from 'node:util';
import Fastify from 'fastify';
import { fromJS } from 'immutable';
import { setRequiredExternalsRegistry } from 'holocron';
Expand Down
2 changes: 1 addition & 1 deletion __tests__/server/shutdown.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* permissions and limitations under the License.
*/

import util from 'util';
import util from 'node:util';

describe('shutdown', () => {
jest.spyOn(global, 'setTimeout').mockImplementation(() => {});
Expand Down
2 changes: 1 addition & 1 deletion __tests__/server/ssrServer-requests.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* permissions and limitations under the License.
*/

import util from 'util';
import util from 'node:util';
import ssrServer from '../../src/server/ssrServer';

const { NODE_ENV } = process.env;
Expand Down
2 changes: 1 addition & 1 deletion __tests__/server/ssrServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* permissions and limitations under the License.
*/

import util from 'util';
import util from 'node:util';
import path from 'path';
import compress from '@fastify/compress';
import Fastify from 'fastify';
Expand Down
2 changes: 1 addition & 1 deletion __tests__/server/utils/createCircuitBreaker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* permissions and limitations under the License.
*/

import util from 'util';
import util from 'node:util';
import CircuitBreaker from 'opossum';
import { getModule } from 'holocron';
import createCircuitBreaker, {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/server/utils/devCdnFactory.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/

/* eslint-disable no-console -- console used in tests */
import util from 'util';
import util from 'node:util';
import fetch from 'node-fetch';
import fs from 'fs';
import rimraf from 'rimraf';
Expand Down
2 changes: 1 addition & 1 deletion __tests__/server/utils/heapdump.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* permissions and limitations under the License.
*/

import util from 'util';
import util from 'node:util';

const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

Expand Down
141 changes: 141 additions & 0 deletions __tests__/server/utils/logging/config/otel.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright 2023 American Express Travel Related Services Company, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import pino from 'pino';
import otelConfig, {
createOtelTransport,
} from '../../../../../src/server/utils/logging/config/otel';
import { serializeError } from '../../../../../src/server/utils/logging/utils';

jest.mock('../../../../../src/server/utils/logging/utils', () => ({
serializeError: jest.fn(),
formatLogEntry: jest.fn((input) => ({ ...input, formatted: true })),
}));

jest.mock('../../../../../src/server/utils/readJsonFile', () => () => ({ buildVersion: 'X.X.X' }));

jest.mock('node:os', () => ({
hostname: () => 'mockHostName',
}));

jest.spyOn(pino, 'transport');

describe('OpenTelemetry logging', () => {
beforeEach(() => jest.clearAllMocks());

describe('config', () => {
it('should serialize errors', () => {
expect(otelConfig.serializers.err).toBe(serializeError);
});

it('should set base data', () => {
expect(otelConfig.base).toMatchInlineSnapshot(`
{
"schemaVersion": "1.0.0",
}
`);
});

it('should set the error key', () => {
expect(otelConfig.errorKey).toBe('error');
});

it('should coerce "log" level to "info" level', () => {
expect(otelConfig.formatters.level('log', 35)).toEqual({ level: 30 });
});

it('should not coerce other levels', () => {
expect(otelConfig.formatters.level('trace', 10)).toEqual({ level: 10 });
expect(otelConfig.formatters.level('debug', 20)).toEqual({ level: 20 });
expect(otelConfig.formatters.level('info', 30)).toEqual({ level: 30 });
expect(otelConfig.formatters.level('warn', 40)).toEqual({ level: 40 });
expect(otelConfig.formatters.level('error', 50)).toEqual({ level: 50 });
});

it('should flatten log objects', () => {
const logEntry = {
foo: {
bar: 'baz',
},
fizz: {
buzz: {
fizzbuzz: true,
},
hello: 'world',
},
};

expect(otelConfig.formatters.log(logEntry)).toMatchInlineSnapshot(`
{
"fizz.buzz.fizzbuzz": true,
"fizz.hello": "world",
"foo.bar": "baz",
"formatted": true,
}
`);
});
});

describe('transport', () => {
process.env.OTEL_SERVICE_NAME = 'Mock Service Name';
process.env.OTEL_SERVICE_NAMESPACE = 'Mock Service Namespace';

it('should include custom resource attributes', () => {
process.env.OTEL_RESOURCE_ATTRIBUTES = 'service.custom.id=XXXXX;deployment.env=qa';
const transport = createOtelTransport();
expect(pino.transport).toHaveBeenCalledTimes(1);
expect(pino.transport.mock.calls[0][0]).toMatchInlineSnapshot(`
{
"options": {
"resourceAttributes": {
"deployment.env": "qa",
"service.custom.id": "XXXXX",
"service.instance.id": "mockHostName",
"service.name": "Mock Service Name",
"service.namespace": "Mock Service Namespace",
"service.version": "X.X.X",
},
},
"target": "pino-opentelemetry-transport",
}
`);
expect(pino.transport.mock.results[0].value).toBe(transport);
});

it('should not throw if there are not custom resource attributes', () => {
delete process.env.OTEL_RESOURCE_ATTRIBUTES;
let transport;
expect(() => {
transport = createOtelTransport();
}).not.toThrow();
expect(pino.transport).toHaveBeenCalledTimes(1);
expect(pino.transport.mock.calls[0][0]).toMatchInlineSnapshot(`
{
"options": {
"resourceAttributes": {
"service.instance.id": "mockHostName",
"service.name": "Mock Service Name",
"service.namespace": "Mock Service Namespace",
"service.version": "X.X.X",
},
},
"target": "pino-opentelemetry-transport",
}
`);
expect(pino.transport.mock.results[0].value).toBe(transport);
});
});
});
2 changes: 1 addition & 1 deletion __tests__/server/utils/logging/config/production.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jest.mock('yargs', () => ({
},
}));

jest.mock('os', () => ({
jest.mock('node:os', () => ({
hostname: () => 'mockHostname',
type: () => 'mockType',
arch: () => 'mockArch',
Expand Down
Loading

0 comments on commit a0b8cb6

Please sign in to comment.