feat: logging - working solution (#2696)

This commit is contained in:
macqbat 2023-01-25 09:43:11 +01:00 committed by GitHub
parent 10fe82dea8
commit a218da93a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 311 additions and 2 deletions

View File

@ -40,10 +40,12 @@ jobs:
- name: See affected apps
run: |
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
echo -n "Affected projects: $affected"
projects=""
if [[ $affected == *"token"* ]]; then projects+='"token-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
if [[ -z "$projects" ]]; then projects+='"token-e2e" "trading-e2e" "explorer-e2e" '; fi
projects=${projects%?}
projects=[${projects// /,}]
echo PROJECTS=$projects >> $GITHUB_ENV

View File

@ -14,6 +14,7 @@ import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
import ApolloLinkTimeout from 'apollo-link-timeout';
import type { GraphQLErrors } from '@apollo/client/errors';
import { localLoggerFactory } from '@vegaprotocol/react-helpers';
const isBrowser = typeof window !== 'undefined';
@ -68,12 +69,12 @@ export function createClient(base?: string, cacheConfig?: InMemoryCacheConfig) {
if (graphQLErrors) {
graphQLErrors.forEach((e) => {
if (e.extensions && e.extensions['type'] !== NOT_FOUND) {
console.log(e);
localLoggerFactory({ application: 'apollo-client' }).error(e);
}
});
}
if (networkError) {
console.log(networkError);
localLoggerFactory({ application: 'apollo-client' }).error(networkError);
}
});

View File

@ -12,3 +12,4 @@ export * from './use-theme-switcher';
export * from './use-storybook-theme-observer';
export * from './use-yesterday';
export * from './use-previous';
export * from './use-logger';

View File

@ -0,0 +1,27 @@
import { useRef } from 'react';
import { BrowserTracing } from '@sentry/tracing';
import * as Sentry from '@sentry/browser';
import type { LocalLogger } from '../lib/local-logger';
import { localLoggerFactory } from '../lib/local-logger';
interface Props {
dsn?: string;
application?: string;
tags?: string[];
}
export const useLogger = ({ dsn, ...props }: Props) => {
const logger = useRef<LocalLogger | null>(null);
if (!logger.current) {
logger.current = localLoggerFactory(props);
if (dsn) {
Sentry.init({
dsn,
integrations: [new BrowserTracing()],
tracesSampleRate: 1,
defaultIntegrations: false,
});
}
}
return logger.current;
};

View File

@ -16,3 +16,4 @@ export * from './lib/is-asset-erc20';
export * from './lib/remove-pagination-wrapper';
export * from './lib/__generated__/ChainId';
export * from './lib/data-grid';
export * from './lib/local-logger';

View File

@ -12,3 +12,4 @@ export * from './time';
export * from './links';
export * from './remove-pagination-wrapper';
export * from './data-grid';
export * from './local-logger';

View File

@ -0,0 +1,113 @@
import * as Sentry from '@sentry/browser';
import type { Severity } from '@sentry/types/types/severity';
import { LocalLogger, localLoggerFactory } from './local-logger';
const methods = [
'debug',
'info',
'log',
'warn',
'error',
'critical',
'fatal',
] as const;
const methodToConsoleMethod = [
'debug',
'info',
'log',
'warn',
'error',
'error',
'error',
] as const;
describe('LocalLogger', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('logger should be properly instantiate', () => {
const logger = localLoggerFactory({});
expect(logger).toBeInstanceOf(LocalLogger);
methods.forEach((method) => {
expect(logger[method]).toBeDefined();
});
});
it('each method should call console', () => {
const methodToLevel = [
'debug',
'info',
'log',
'warning',
'error',
'critical',
'fatal',
];
const logger = localLoggerFactory({ logLevel: 'debug' });
methods.forEach((method, i) => {
const consoleMethod = methodToConsoleMethod[i];
jest.spyOn(console, consoleMethod).mockImplementation();
logger[method]('test', 'test2');
expect(console[consoleMethod]).toHaveBeenCalledWith(
`trading:${methodToLevel[i]}: `,
'test',
'test2'
);
jest.clearAllMocks();
});
});
it('each method should call sentry', () => {
jest.spyOn(Sentry, 'captureMessage');
jest.spyOn(Sentry, 'captureException');
const logger = localLoggerFactory({ logLevel: 'debug' });
methods.forEach((method, i) => {
jest.spyOn(console, methodToConsoleMethod[i]).mockImplementation();
logger[method]('test', 'test2');
/* eslint-disable jest/no-conditional-expect */
if (i < 4) {
expect(Sentry.captureMessage).toHaveBeenCalled();
expect(Sentry.captureException).not.toHaveBeenCalled();
} else {
expect(Sentry.captureMessage).not.toHaveBeenCalled();
expect(Sentry.captureException).toHaveBeenCalled();
/* eslint-enable jest/no-conditional-expect */
}
jest.clearAllMocks();
});
});
it('breadcrumb method should call Sentry', () => {
const logger = localLoggerFactory({});
jest.spyOn(Sentry, 'addBreadcrumb');
const breadCrumb = {
type: 'type',
level: 'fatal' as Severity,
event_id: 'event_id',
category: 'category',
message: 'message',
data: {
data_1: 'data_1',
data_2: 'data_2',
},
timestamp: 1111111,
};
logger.addSentryBreadcrumb(breadCrumb);
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith(breadCrumb);
});
it('setLogLevel should change log level', () => {
const logger = localLoggerFactory({ logLevel: 'info' });
jest.spyOn(console, 'debug').mockImplementation();
logger.debug('test', 'test1');
expect(console.debug).not.toHaveBeenCalled();
logger.setLogLevel('debug');
logger.debug('test', 'test1');
expect(console.debug).toHaveBeenCalledWith(
'trading:debug: ',
'test',
'test1'
);
});
});

View File

@ -0,0 +1,163 @@
import * as Sentry from '@sentry/browser';
import type { Scope } from '@sentry/browser';
import type { Severity, Breadcrumb, Primitive } from '@sentry/types';
const LogLevels = [
'fatal',
'error',
'warning',
'log',
'info',
'debug',
'critical',
'silent',
];
type LogLevelsType = typeof LogLevels[number];
type ConsoleArg = string | number | boolean | bigint | symbol | object;
type ConsoleMethod = {
[K in keyof Console]: Console[K] extends (...args: ConsoleArg[]) => unknown
? K
: never;
}[keyof Console] &
string;
interface LoggerConf {
application?: string;
tags?: string[];
logLevel?: LogLevelsType;
}
const isPrimitive = (arg: ConsoleArg | undefined | null): arg is Primitive => {
return ['string', 'number', 'boolean', 'bigint', 'symbol'].includes(
typeof arg
);
};
export class LocalLogger {
static levelLogMap: Record<LogLevelsType, number> = {
debug: 10,
info: 20,
log: 30,
warning: 40,
error: 50,
critical: 60,
fatal: 70,
silent: 80,
};
private _logLevel: LogLevelsType = 'info';
private get numberLogLevel() {
return LocalLogger.levelLogMap[this._logLevel];
}
private tags: string[] = [];
private application = 'trading';
constructor(conf: LoggerConf) {
if (conf.application) {
this.application = conf.application;
}
this.tags = [...(conf.tags || [])];
this._logLevel = conf.logLevel || this._logLevel;
}
public debug(...args: ConsoleArg[]) {
this._log('debug', 'debug', args);
}
public info(...args: ConsoleArg[]) {
this._log('info', 'info', args);
}
public log(...args: ConsoleArg[]) {
this._log('log', 'log', args);
}
public warn(...args: ConsoleArg[]) {
this._log('warning', 'warn', args);
}
public error(...args: ConsoleArg[]) {
this._log('error', 'error', args);
}
public critical(...args: ConsoleArg[]) {
this._log('critical', 'error', args);
}
public fatal(...args: ConsoleArg[]) {
this._log('fatal', 'error', args);
}
private _log(
level: LogLevelsType,
logMethod: ConsoleMethod,
args: ConsoleArg[]
) {
if (this.numberLogLevel <= LocalLogger.levelLogMap[level]) {
console[logMethod].apply(console, [
`${this.application}:${level}: `,
...args,
]);
}
this._transmit(level, args);
}
private _extractArgs(
level: LogLevelsType,
args: ConsoleArg[]
): [string, Error, Scope] {
const arg = args.shift();
const error = arg instanceof Error ? arg : null;
const msg = error ? error.message : String(arg);
const scope = new Sentry.Scope();
scope.setLevel(level as Severity);
let logArgs: Record<string, unknown>;
try {
logArgs = { args: JSON.stringify(args) };
} catch (e) {
logArgs = { args };
}
scope.setContext('event-record', logArgs);
if (this.tags.length) {
this.tags.forEach((tag) => {
const found = args.reduce((aggr, arg) => {
if (typeof arg === 'object' && tag in arg) {
// @ts-ignore change object to record
aggr = arg[tag] as unknown as Primitive | object;
}
return aggr;
}, null as Primitive | object);
if (isPrimitive(found)) {
scope.setTag(tag, found);
}
});
}
return [msg, error || new Error(msg), scope];
}
private _transmit(level: LogLevelsType, args: ConsoleArg[]) {
const [msg, error, logEvent] = this._extractArgs(level, args);
switch (level) {
case 'debug':
case 'info':
case 'log':
case 'warning':
Sentry.captureMessage(msg, logEvent);
return;
case 'error':
case 'critical':
case 'fatal':
Sentry.captureException(error, logEvent);
return;
}
}
public addSentryBreadcrumb(breadcrumb: Breadcrumb) {
Sentry.addBreadcrumb(breadcrumb);
}
public setLogLevel(logLevel: LogLevelsType) {
this._logLevel = logLevel;
}
public get logLevel() {
return this._logLevel;
}
}
let singleLoggerInstance: LocalLogger;
export const localLoggerFactory = (conf: LoggerConf) => {
if (!singleLoggerInstance) {
singleLoggerInstance = new LocalLogger(conf);
}
if (conf.logLevel && singleLoggerInstance.logLevel !== conf.logLevel) {
singleLoggerInstance.setLogLevel(conf.logLevel);
}
return singleLoggerInstance;
};