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

v5 - feat(otel): log notice to STDOUT when using OTel #1233

Merged
merged 1 commit into from
Jan 2, 2024
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
30 changes: 0 additions & 30 deletions __tests__/server/__snapshots__/index.spec.js.snap

This file was deleted.

171 changes: 141 additions & 30 deletions __tests__/server/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jest.unmock('yargs');
describe('server index', () => {
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
jest.spyOn(process.stdout, 'write').mockImplementation(() => {});
jest.spyOn(process.stderr, 'write').mockImplementation(() => {});
const origFsExistsSync = fs.existsSync;

let addServer;
Expand All @@ -48,7 +50,14 @@ describe('server index', () => {

jest.doMock('@americanexpress/one-app-dev-proxy', () => jest.fn(() => ({
listen: jest.fn((port, cb) => {
setTimeout(() => (oneAppDevProxyError ? cb(new Error('test error')) : cb(null, { port })));
setTimeout(() => {
if (oneAppDevProxyError) {
const error = new Error('dev proxy server test error');
error.stack = `${error.toString()}\n at <anonymous>:1:1`;
return cb(error);
}
return cb(null, { port });
});
return { close: 'one-app-dev-proxy' };
}),
}))
Expand All @@ -61,10 +70,7 @@ describe('server index', () => {

jest.doMock('cross-fetch');

jest.doMock(
'../../src/server/utils/loadModules',
() => jest.fn(() => Promise.resolve())
);
jest.doMock('../../src/server/utils/loadModules', () => jest.fn(() => Promise.resolve()));
jest.doMock('babel-polyfill', () => {
// jest includes babel-polyfill
// if included twice, babel-polyfill will complain that it should be only once
Expand All @@ -76,20 +82,33 @@ describe('server index', () => {
// ms timeouts are to ensure order
jest.doMock('../../src/server/ssrServer', () => ({
listen: jest.fn((port, cb) => {
setTimeout(() => (ssrServerError ? cb(new Error('test error')) : cb(null, { port })), 50);
setTimeout(() => {
if (ssrServerError) {
const error = new Error('ssr server test error');
error.stack = `${error.toString()}\n at <anonymous>:1:1`;
return cb(error);
}
return cb(null, { port });
}, 50);
return { close: 'ssrServer' };
}),
}));
jest.doMock('../../src/server/metricsServer', () => ({
listen: jest.fn((port, cb) => {
setTimeout(() => (metricsServerError ? cb(new Error('test error')) : cb(null)), 10);
setTimeout(() => {
if (metricsServerError) {
const error = new Error('metrics server test error');
error.stack = `${error.toString()}\n at <anonymous>:1:1`;
return cb(error);
}
return cb(null);
}, 10);
return { close: 'metricsServer' };
}),
}));

jest.doMock('../../src/server/listen', () => jest.fn(
(app, cb) => app.listen(3000, (err) => cb(err, { port: 3000 }))
));
jest.doMock('../../src/server/listen', () => jest.fn((app, cb) => app.listen(3000, (err) => cb(err, { port: 3000 })))
);

addServer = jest.fn();
shutdown = jest.fn();
Expand All @@ -103,7 +122,14 @@ describe('server index', () => {
jest.doMock('../../src/server/devHolocronCDN', () => ({
default: {
listen: jest.fn((port, cb) => {
setTimeout(() => (devHolocronCdnError ? cb(new Error('test error')) : cb(null, { port })));
setTimeout(() => {
if (devHolocronCdnError) {
const error = new Error('dev cdn server test error');
error.stack = `${error.toString()}\n at <anonymous>:1:1`;
return cb(error);
}
return cb(null, { port });
});
return { close: 'devHolocronCdn' };
}),
},
Expand Down Expand Up @@ -133,8 +159,23 @@ describe('server index', () => {
load();
const yargs = require('yargs');

expect(yargs.getOptions().boolean).toMatchSnapshot();
expect(yargs.getOptions().string).toMatchSnapshot();
expect(yargs.getOptions().boolean).toMatchInlineSnapshot(`
Array [
"help",
"version",
"m",
"use-middleware",
"use-host",
]
`);
expect(yargs.getOptions().string).toMatchInlineSnapshot(`
Array [
"root-module-name",
"module-map-url",
"log-format",
"log-level",
]
`);
});

it('starts devHolocronCDN on port 4011', async () => {
Expand All @@ -160,32 +201,40 @@ describe('server index', () => {
const endpointsFilePath = path.join(process.cwd(), '.dev', 'endpoints', 'index.js');
process.env.NODE_ENV = 'development';
fs.existsSync = () => true;
jest.doMock(endpointsFilePath, () => () => ({
oneTestEndpointUrl: {
devProxyPath: 'test',
destination: 'https://example.com',
},
}),
{ virtual: true }
jest.doMock(
endpointsFilePath,
() => () => ({
oneTestEndpointUrl: {
devProxyPath: 'test',
destination: 'https://example.com',
},
}),
{ virtual: true }
);
await load();
fs.existsSync = origFsExistsSync;
const oneAppDevProxy = require('@americanexpress/one-app-dev-proxy');
expect(oneAppDevProxy).toHaveBeenCalledTimes(1);
expect(oneAppDevProxy.mock.calls[0][0].remotes).toMatchSnapshot();
expect(oneAppDevProxy.mock.calls[0][0].remotes).toMatchInlineSnapshot(`
Object {
"test": "https://example.com",
}
`);
});

it('starts one-app-dev-proxy with out any remotes if there is no module endpoints file provided', async () => {
const endpointsFilePath = path.join(process.cwd(), '.dev', 'endpoints', 'index.js');
process.env.NODE_ENV = 'development';
fs.existsSync = () => false;
jest.doMock(endpointsFilePath, () => () => ({
oneTestEndpointUrl: {
devProxyPath: 'test',
destination: 'https://example.com',
},
}),
{ virtual: true }
jest.doMock(
endpointsFilePath,
() => () => ({
oneTestEndpointUrl: {
devProxyPath: 'test',
destination: 'https://example.com',
},
}),
{ virtual: true }
);
await load();
fs.existsSync = origFsExistsSync;
Expand Down Expand Up @@ -353,7 +402,29 @@ describe('server index', () => {
}

expect(console.error).toHaveBeenCalled();
expect(console.error.mock.calls[0][0]).toMatchSnapshot();
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(
'[Error: ssr server test error]'
);
});

it('does not log a notice directly to STDERR when not using OTel and listening on the server fails', async () => {
delete process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
await load({ ssrServerError: true });
expect(process.stderr.write).not.toHaveBeenCalled();
});

it('logs a notice directly to STDERR when using OTel and listening on the server fails', async () => {
process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://0.0.0.0:4317/v1/logs';
await load({ ssrServerError: true });
expect(process.stderr.write).toHaveBeenCalledTimes(1);
expect(process.stderr.write.mock.calls[0][0]).toMatchInlineSnapshot(`
"
one-app failed to start. Logs are being sent to OTel via gRPC at http://0.0.0.0:4317/v1/logs

Error: ssr server test error
at <anonymous>:1:1"
`);
delete process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
});

it('logs errors when listening on the metrics server fails', async () => {
Expand All @@ -364,7 +435,29 @@ describe('server index', () => {
}

expect(console.error).toHaveBeenCalled();
expect(console.error.mock.calls[0][0]).toMatchSnapshot();
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(
'"error encountered starting the metrics server"'
);
});

it('does not log a notice directly to STDERR when not using OTel and listening on the metrics server fails', async () => {
delete process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
await load({ metricsServerError: true });
expect(process.stderr.write).not.toHaveBeenCalled();
});

it('logs a notice directly to STDERR when using OTel and listening on the metrics server fails', async () => {
process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://0.0.0.0:4317/v1/logs';
await load({ metricsServerError: true });
expect(process.stderr.write).toHaveBeenCalledTimes(1);
expect(process.stderr.write.mock.calls[0][0]).toMatchInlineSnapshot(`
"
one-app failed to start. Logs are being sent to OTel via gRPC at http://0.0.0.0:4317/v1/logs

Error: metrics server test error
at <anonymous>:1:1"
`);
delete process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
});

it('closes servers when starting ssrServer fails', async () => {
Expand Down Expand Up @@ -403,6 +496,24 @@ describe('server index', () => {
expect(console.log.mock.calls[0][0]).toMatch('📊 Metrics server listening on port 3005');
});

it('does not log a notice to STDOUT when not using OTel', async () => {
delete process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
await load();
expect(process.stdout.write).not.toHaveBeenCalled();
});

it('logs a notice to STDOUT when using OTel', async () => {
process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://0.0.0.0:4317/v1/logs';
await load();
expect(process.stdout.write).toHaveBeenCalledTimes(1);
expect(process.stdout.write.mock.calls[0][0]).toMatchInlineSnapshot(`
"
one-app started successfully. Logs are being sent to OTel via gRPC at http://0.0.0.0:4317/v1/logs
"
`);
delete process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
});

it('initiates module-map polling if successfully listening on port', async () => {
try {
await load();
Expand Down
10 changes: 9 additions & 1 deletion src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* permissions and limitations under the License.
*/

import util from 'node:util';
import path from 'path';
import fs from 'fs';
import Intl from 'lean-intl';
Expand Down Expand Up @@ -64,7 +65,11 @@ function appServersStart() {
return Promise.all([
ssrServerStart(),
metricsServerStart(),
]);
]).then(() => {
if (process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) {
process.stdout.write(util.format('\none-app started successfully. Logs are being sent to OTel via gRPC at %s\n', process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT));
}
});
}

let serverChain;
Expand Down Expand Up @@ -128,5 +133,8 @@ if (process.env.NODE_ENV === 'development') {

export default serverChain.catch((err) => {
console.error(err);
if (process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) {
process.stderr.write(util.format('\none-app failed to start. Logs are being sent to OTel via gRPC at %s\n\n%s', process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, err.stack));
}
return shutdown();
});