feat: logging - working solution (#2696)
This commit is contained in:
parent
10fe82dea8
commit
a218da93a6
2
.github/workflows/cypress-pr.yml
vendored
2
.github/workflows/cypress-pr.yml
vendored
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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';
|
||||
|
27
libs/react-helpers/src/hooks/use-logger.ts
Normal file
27
libs/react-helpers/src/hooks/use-logger.ts
Normal 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;
|
||||
};
|
@ -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';
|
||||
|
@ -12,3 +12,4 @@ export * from './time';
|
||||
export * from './links';
|
||||
export * from './remove-pagination-wrapper';
|
||||
export * from './data-grid';
|
||||
export * from './local-logger';
|
||||
|
113
libs/react-helpers/src/lib/local-logger.spec.ts
Normal file
113
libs/react-helpers/src/lib/local-logger.spec.ts
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
163
libs/react-helpers/src/lib/local-logger.ts
Normal file
163
libs/react-helpers/src/lib/local-logger.ts
Normal 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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user