Skip to content

Commit 83122aa

Browse files
committed
feat(log): add transports support from private impl
1 parent d790d20 commit 83122aa

File tree

8 files changed

+232
-7
lines changed

8 files changed

+232
-7
lines changed

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
**/dist
22
**/bin
3+
4+
packages/log/recipes

packages/log/recipes/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# @dot/log Recipes
2+
3+
This directory contains recipes for extending @dot/log with additional transports and functionality.
4+
5+
## Available Recipes
6+
7+
- [Better Stack Transport](./better-stack.ts) - Integration with Better Stack logging service
8+
- [Sentry Transport](./sentry.ts) - Integration with Sentry error tracking and performance monitoring
9+
10+
Each recipe provides a custom Transport implementation that can be used with @dot/log to send logs to additional services beyond the default console output.
11+
12+
To use a recipe, import the transport class and add it to the transports array when creating a logger:
13+
14+
```ts
15+
import { getLog } from '@dot/log';
16+
import { SentryTransport } from '@dot/log/recipes/sentry';
17+
18+
const log = getLog({
19+
name: 'my-app',
20+
transports: [new SentryTransport()]
21+
});
22+
```
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Logtail } from '@logtail/node';
2+
import type { StackContextHint } from '@logtail/types';
3+
import type { MethodFactoryLevels } from 'loglevelnext';
4+
5+
import { Transport, type LogMethodOptions, type SendLogOptions } from '@dot/log';
6+
7+
const stackContextHint = {
8+
fileName: '@rally/log/transports/server/better-stack',
9+
methodNames: ['log', 'error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly']
10+
} satisfies StackContextHint;
11+
const { warn } = console;
12+
13+
export class BetterStackTransport extends Transport {
14+
private logtail: Logtail | undefined = void 0;
15+
16+
constructor() {
17+
super();
18+
19+
const { BETTERSTACK_TOKEN, DISABLE_BETTERSTACK, RALLY_LOG_LEVEL } = process.env as Record<
20+
string,
21+
string
22+
>;
23+
const logLevel = RALLY_LOG_LEVEL as MethodFactoryLevels;
24+
25+
if (!BETTERSTACK_TOKEN || !!DISABLE_BETTERSTACK) {
26+
if (logLevel === 'debug' || logLevel === 'trace') {
27+
if (DISABLE_BETTERSTACK)
28+
warn(`@rally/log → betterstack: disabled by way of the DISABLE_BETTERSTACK envar`);
29+
}
30+
return;
31+
}
32+
33+
this.logtail = new Logtail(BETTERSTACK_TOKEN);
34+
}
35+
36+
send({ args, methodName }: SendLogOptions) {
37+
if (!this.logtail) return;
38+
39+
let data = { rallyEnv: process.env.DEPLOY_ENV };
40+
const [message, ...rest] = args;
41+
const [maybeOptions] = rest.slice(-1) as LogMethodOptions[];
42+
43+
if (maybeOptions?.data) {
44+
data = { ...data, ...maybeOptions.data };
45+
rest.pop();
46+
}
47+
48+
this.logtail.log(message, methodName, data, stackContextHint);
49+
this.logtail.flush();
50+
}
51+
}

packages/log/recipes/sentry.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { default as SentryType, Event, Scope } from '@sentry/node';
2+
import type { MethodFactoryLevels } from 'loglevelnext';
3+
4+
import { Transport, type LogMethodOptions, type SendLogOptions } from '@dot/log';
5+
6+
interface ReportOptions {
7+
args: any[];
8+
methodName: string;
9+
sentryInstance: Promise<typeof SentryType | undefined>;
10+
}
11+
12+
const { warn } = console;
13+
const appEnv: Record<string, string> = {
14+
default: 'unknown',
15+
development: 'dev',
16+
production: 'prod',
17+
test: 'test'
18+
};
19+
const isLambda = Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME);
20+
21+
const initSentry = async () => {
22+
if (typeof process === 'undefined') return void 0;
23+
24+
const {
25+
DISABLE_SENTRY,
26+
NODE_ENV,
27+
RALLY_LOG_LEVEL,
28+
SENTRY_DSN,
29+
SENTRY_MODULE,
30+
SENTRY_SAMPLE_RATE
31+
} = process.env as Record<string, string>;
32+
const logLevel = RALLY_LOG_LEVEL as MethodFactoryLevels;
33+
34+
if (!SENTRY_DSN || !!DISABLE_SENTRY) {
35+
if (logLevel === 'debug' || logLevel === 'trace') {
36+
if (DISABLE_SENTRY) warn(`@rally/log → sentry: disabled by way of the DISABLE_SENTRY envar`);
37+
}
38+
return void 0;
39+
}
40+
41+
const dsn = SENTRY_DSN;
42+
const enabled = ['prod', 'production'].includes(NODE_ENV);
43+
const environment = appEnv[NODE_ENV || 'default'];
44+
const moduleName = SENTRY_MODULE;
45+
const sampleRate = parseFloat(SENTRY_SAMPLE_RATE || '1');
46+
47+
const { default: Sentry } = await import('@sentry/node');
48+
const beforeSend = ((event: Event) => {
49+
return { ...event, tags: { module: moduleName, ...event.tags } };
50+
}) as any;
51+
const initMethod = isLambda ? Sentry.initWithoutDefaultIntegrations : Sentry.init;
52+
53+
initMethod({
54+
attachStacktrace: true,
55+
beforeSend,
56+
dsn,
57+
enabled,
58+
environment,
59+
sampleRate
60+
});
61+
62+
return Sentry;
63+
};
64+
65+
const report = async (options: ReportOptions) => {
66+
const { args, methodName, sentryInstance } = options;
67+
68+
const tags = { rallyEnv: process.env.DEPLOY_ENV };
69+
const [message, ...rest] = args;
70+
const [maybeOptions] = rest.slice(-1) as LogMethodOptions[];
71+
const event: Event = { extra: { args: rest }, message, tags };
72+
const sentry = await sentryInstance;
73+
74+
if (!sentry) return;
75+
76+
if (maybeOptions && maybeOptions.data) {
77+
event.tags = { ...maybeOptions.data };
78+
rest.pop();
79+
}
80+
81+
if (methodName === 'warn') {
82+
sentry.captureEvent({ level: 'warning', ...event });
83+
} else if (methodName === 'error') {
84+
sentry.withScope((scope: Scope) => {
85+
scope.setExtras(event as any);
86+
scope.setLevel('error');
87+
scope.setTags({ message, ...event.tags });
88+
89+
// Note: The typing in @sentry/types is incorrect: https://github.com/getsentry/sentry-javascript/issues/5764
90+
// We need to assert that this is a truthy value
91+
if (message) sentry.captureException(message);
92+
else warn('@sa/log → sentry: message was falsy:', event);
93+
});
94+
}
95+
};
96+
97+
export class SentryTransport extends Transport {
98+
private sentryInstance: Promise<typeof SentryType | undefined> | undefined = void 0;
99+
100+
constructor() {
101+
super();
102+
103+
this.sentryInstance = initSentry();
104+
}
105+
106+
send({ args, methodName }: SendLogOptions) {
107+
if (!this.sentryInstance) return;
108+
report({ args, methodName, sentryInstance: this.sentryInstance });
109+
}
110+
}

packages/log/src/LogFactory.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,43 @@
1-
import { PrefixFactory, PrefixFactoryOptions } from 'loglevelnext';
2-
import { DeferredPromise } from 'p-defer';
1+
import type { PrefixFactoryOptions } from 'loglevelnext';
2+
import { PrefixFactory } from 'loglevelnext';
3+
import type { DeferredPromise } from 'p-defer';
4+
5+
import type { Transport } from './Transport';
36

47
interface FactoryOptions {
58
ready: DeferredPromise<unknown>;
9+
transports?: Transport[];
610
}
711

12+
export type LogMethodArgs = [string, ...unknown[]];
13+
14+
const NOOP = '() => { }';
15+
816
export class LogFactory extends PrefixFactory {
917
private readonly ready: DeferredPromise<unknown>;
18+
private readonly transports: Transport[];
1019

1120
constructor(options: PrefixFactoryOptions & FactoryOptions) {
1221
super(void 0, options);
1322

1423
this.ready = options.ready;
24+
this.transports = options.transports || [];
25+
}
26+
27+
override make(methodName: any) {
28+
const og = super.make(methodName);
29+
return (...args: LogMethodArgs) => {
30+
// is the method a noop?
31+
if (og.toString() !== NOOP) {
32+
this.transports.forEach((transport) => transport.send({ args, methodName }));
33+
}
34+
35+
// call the original method and output to console
36+
og(...args);
37+
};
1538
}
1639

17-
replaceMethods(level: number | string) {
40+
override replaceMethods(level: number | string) {
1841
super.replaceMethods(level);
1942
this.ready?.resolve();
2043
}

packages/log/src/Transport.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { MethodFactoryLevels } from 'loglevelnext';
2+
3+
export interface LogMethodOptions {
4+
data: Record<string, string | number>;
5+
}
6+
7+
export interface SendLogOptions {
8+
args: [string, ...unknown[]];
9+
methodName: MethodFactoryLevels;
10+
}
11+
12+
export abstract class Transport {
13+
abstract send(options: SendLogOptions): void;
14+
}

packages/log/src/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import loglevel, { MethodFactoryLevels } from 'loglevelnext';
33
import defer from 'p-defer';
44

55
import { LogFactory } from './LogFactory';
6+
import { type Transport } from './Transport';
67

78
export type { MethodFactoryLevels };
8-
9+
export * from './Transport';
910
export { LogFactory };
1011

1112
export interface LogOptions {
1213
brand?: string;
1314
name: string;
15+
transports?: Transport[];
1416
}
1517

1618
type LogIndex = {
@@ -52,12 +54,13 @@ export const getLog = (opts?: LogOptions) => {
5254
level: ({ level }: { level: string }) => colors[level],
5355
ready,
5456
template,
55-
time: () => new Date().toTimeString().split(' ')[0]
57+
time: () => new Date().toTimeString().split(' ')[0],
58+
transports: options.transports
5659
} as any);
5760
const { DOT_LOG_LEVEL = 'info' } = typeof process === 'undefined' ? defaultEnv : process.env;
5861
const logOptions = {
5962
factory,
60-
level: DOT_LOG_LEVEL,
63+
level: DOT_LOG_LEVEL || 'info',
6164
name: `dot-log:${options.name}`
6265
};
6366

shared/tsconfig.base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@
2222
"target": "es2020",
2323
"useUnknownInCatchVariables": false
2424
},
25-
"exclude": ["dist", "node_modules"]
25+
"exclude": ["dist", "node_modules", "**/recipes"]
2626
}

0 commit comments

Comments
 (0)