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
|
- name: See affected apps
|
||||||
run: |
|
run: |
|
||||||
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
|
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
|
||||||
|
echo -n "Affected projects: $affected"
|
||||||
projects=""
|
projects=""
|
||||||
if [[ $affected == *"token"* ]]; then projects+='"token-e2e" '; fi
|
if [[ $affected == *"token"* ]]; then projects+='"token-e2e" '; fi
|
||||||
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
|
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
|
||||||
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-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%?}
|
||||||
projects=[${projects// /,}]
|
projects=[${projects// /,}]
|
||||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
echo PROJECTS=$projects >> $GITHUB_ENV
|
||||||
|
@ -14,6 +14,7 @@ import { onError } from '@apollo/client/link/error';
|
|||||||
import { RetryLink } from '@apollo/client/link/retry';
|
import { RetryLink } from '@apollo/client/link/retry';
|
||||||
import ApolloLinkTimeout from 'apollo-link-timeout';
|
import ApolloLinkTimeout from 'apollo-link-timeout';
|
||||||
import type { GraphQLErrors } from '@apollo/client/errors';
|
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||||
|
import { localLoggerFactory } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
const isBrowser = typeof window !== 'undefined';
|
const isBrowser = typeof window !== 'undefined';
|
||||||
|
|
||||||
@ -68,12 +69,12 @@ export function createClient(base?: string, cacheConfig?: InMemoryCacheConfig) {
|
|||||||
if (graphQLErrors) {
|
if (graphQLErrors) {
|
||||||
graphQLErrors.forEach((e) => {
|
graphQLErrors.forEach((e) => {
|
||||||
if (e.extensions && e.extensions['type'] !== NOT_FOUND) {
|
if (e.extensions && e.extensions['type'] !== NOT_FOUND) {
|
||||||
console.log(e);
|
localLoggerFactory({ application: 'apollo-client' }).error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (networkError) {
|
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-storybook-theme-observer';
|
||||||
export * from './use-yesterday';
|
export * from './use-yesterday';
|
||||||
export * from './use-previous';
|
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/remove-pagination-wrapper';
|
||||||
export * from './lib/__generated__/ChainId';
|
export * from './lib/__generated__/ChainId';
|
||||||
export * from './lib/data-grid';
|
export * from './lib/data-grid';
|
||||||
|
export * from './lib/local-logger';
|
||||||
|
@ -12,3 +12,4 @@ export * from './time';
|
|||||||
export * from './links';
|
export * from './links';
|
||||||
export * from './remove-pagination-wrapper';
|
export * from './remove-pagination-wrapper';
|
||||||
export * from './data-grid';
|
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