@drawbridge/drawbridge-telemetry 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bullmq.cjs CHANGED
@@ -38,6 +38,7 @@ var import_crypto = require("crypto");
38
38
 
39
39
  // index.js
40
40
  var Sentry = __toESM(require("@sentry/node"), 1);
41
+ var import_pino = __toESM(require("pino"), 1);
41
42
  var withTraceScope = (traceId, fn) => Sentry.withScope((scope) => {
42
43
  if (traceId) scope.setTag("traceId", traceId);
43
44
  return fn(scope);
@@ -76,28 +77,46 @@ var enqueueFromWorker = async (queue, name, data, options = {}) => {
76
77
  options
77
78
  );
78
79
  };
79
- var attachQueueEventsLogger = (queueEvents, queueName, logger2) => {
80
- if (!(logger2 == null ? void 0 : logger2.event)) return;
81
- queueEvents.on("completed", ({ jobId, returnvalue, prev }) => {
82
- logger2.event("job.completed", {
83
- jobId,
84
- queue: queueName,
85
- prev
86
- });
80
+ var attachQueueEventsLogger = (queue, queueEvents, logger2) => {
81
+ if (!(logger2 == null ? void 0 : logger2.emit)) return;
82
+ const queueName = queue == null ? void 0 : queue.name;
83
+ queueEvents.on("completed", async ({ jobId }) => {
84
+ try {
85
+ const job = await queue.getJob(jobId).catch(() => null);
86
+ const duration = (job == null ? void 0 : job.finishedOn) && (job == null ? void 0 : job.processedOn) ? job.finishedOn - job.processedOn : null;
87
+ await logger2.emit("job.completed", {
88
+ jobId,
89
+ queue: queueName,
90
+ name: job == null ? void 0 : job.name,
91
+ duration,
92
+ attemptsMade: job == null ? void 0 : job.attemptsMade
93
+ });
94
+ } catch {
95
+ }
87
96
  });
88
- queueEvents.on("failed", ({ jobId, failedReason, prev }) => {
89
- logger2.event("job.failed", {
90
- jobId,
91
- queue: queueName,
92
- failedReason,
93
- prev
94
- });
97
+ queueEvents.on("failed", async ({ jobId, failedReason }) => {
98
+ try {
99
+ const job = await queue.getJob(jobId).catch(() => null);
100
+ await logger2.emit("job.failed", {
101
+ jobId,
102
+ queue: queueName,
103
+ name: job == null ? void 0 : job.name,
104
+ failedReason,
105
+ attemptsMade: job == null ? void 0 : job.attemptsMade
106
+ });
107
+ } catch {
108
+ }
95
109
  });
96
- queueEvents.on("stalled", ({ jobId }) => {
97
- logger2.event("job.stalled", {
98
- jobId,
99
- queue: queueName
100
- });
110
+ queueEvents.on("stalled", async ({ jobId }) => {
111
+ try {
112
+ const job = await queue.getJob(jobId).catch(() => null);
113
+ await logger2.emit("job.stalled", {
114
+ jobId,
115
+ queue: queueName,
116
+ name: job == null ? void 0 : job.name
117
+ });
118
+ } catch {
119
+ }
101
120
  });
102
121
  };
103
122
  // Annotate the CommonJS export names for ESM import in node:
package/dist/bullmq.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'crypto';
2
2
  import { currentTraceId, withTraceScope } from './index.cjs';
3
3
  import '@sentry/node';
4
+ import 'pino';
4
5
 
5
6
  // Wrap a BullMQ worker handler so it runs inside a Sentry scope tagged
6
7
  // with the job's traceId, queue name, and job name. Matches BullMQ's
@@ -60,42 +61,80 @@ const enqueueFromWorker = async ( queue, name, data, options = {} ) => {
60
61
 
61
62
  };
62
63
 
63
- // Attach a QueueEvents listener that emits structured `job.completed` and
64
- // `job.failed` records to the active Sentry logger. Use one per queue.
65
- // Stage 2 only Stage 1 ships the helper but it's a no-op until the
66
- // unified logger lands.
64
+ // Attach a QueueEvents listener that emits `job.completed`, `job.failed`,
65
+ // and `job.stalled` to the unified logger. Called once per (queue, events)
66
+ // pair, typically right after both are constructed.
67
+ //
68
+ // The Queue handle is required because BullMQ's QueueEvents payload doesn't
69
+ // include duration, attempts, or job name on its own — we look those up via
70
+ // queue.getJob( jobId ). The lookup is best-effort: jobs that have already
71
+ // been swept by removeOnComplete / removeOnFail return null, in which case
72
+ // the emit goes out with the fields we do have.
73
+ //
74
+ // duration is measured from worker pickup (processedOn) to finish
75
+ // (finishedOn), i.e. execution time. Queue-wait time (timestamp to
76
+ // processedOn) is not included; if that becomes interesting it can be
77
+ // added as a separate field.
78
+
79
+ const attachQueueEventsLogger = ( queue, queueEvents, logger ) => {
80
+
81
+ if( ! logger?.emit ) return;
82
+
83
+ const queueName = queue?.name;
84
+
85
+ queueEvents.on( 'completed', async ({ jobId }) => {
86
+
87
+ try {
67
88
 
68
- const attachQueueEventsLogger = ( queueEvents, queueName, logger ) => {
89
+ const job = await queue.getJob( jobId ).catch( () => null );
69
90
 
70
- if( ! logger?.event ) return;
91
+ const duration = ( job?.finishedOn && job?.processedOn )
92
+ ? job.finishedOn - job.processedOn
93
+ : null;
71
94
 
72
- queueEvents.on( 'completed', ({ jobId, returnvalue, prev }) => {
95
+ await logger.emit( 'job.completed', {
96
+ jobId,
97
+ queue : queueName,
98
+ name : job?.name,
99
+ duration,
100
+ attemptsMade : job?.attemptsMade
101
+ });
73
102
 
74
- logger.event( 'job.completed', {
75
- jobId,
76
- queue : queueName,
77
- prev
78
- });
103
+ } catch {}
79
104
 
80
105
  });
81
106
 
82
- queueEvents.on( 'failed', ({ jobId, failedReason, prev }) => {
107
+ queueEvents.on( 'failed', async ({ jobId, failedReason }) => {
83
108
 
84
- logger.event( 'job.failed', {
85
- jobId,
86
- queue : queueName,
87
- failedReason,
88
- prev
89
- });
109
+ try {
110
+
111
+ const job = await queue.getJob( jobId ).catch( () => null );
112
+
113
+ await logger.emit( 'job.failed', {
114
+ jobId,
115
+ queue : queueName,
116
+ name : job?.name,
117
+ failedReason,
118
+ attemptsMade : job?.attemptsMade
119
+ });
120
+
121
+ } catch {}
90
122
 
91
123
  });
92
124
 
93
- queueEvents.on( 'stalled', ({ jobId }) => {
125
+ queueEvents.on( 'stalled', async ({ jobId }) => {
126
+
127
+ try {
128
+
129
+ const job = await queue.getJob( jobId ).catch( () => null );
130
+
131
+ await logger.emit( 'job.stalled', {
132
+ jobId,
133
+ queue : queueName,
134
+ name : job?.name
135
+ });
94
136
 
95
- logger.event( 'job.stalled', {
96
- jobId,
97
- queue : queueName
98
- });
137
+ } catch {}
99
138
 
100
139
  });
101
140
 
package/dist/bullmq.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'crypto';
2
2
  import { currentTraceId, withTraceScope } from './index.js';
3
3
  import '@sentry/node';
4
+ import 'pino';
4
5
 
5
6
  // Wrap a BullMQ worker handler so it runs inside a Sentry scope tagged
6
7
  // with the job's traceId, queue name, and job name. Matches BullMQ's
@@ -60,42 +61,80 @@ const enqueueFromWorker = async ( queue, name, data, options = {} ) => {
60
61
 
61
62
  };
62
63
 
63
- // Attach a QueueEvents listener that emits structured `job.completed` and
64
- // `job.failed` records to the active Sentry logger. Use one per queue.
65
- // Stage 2 only Stage 1 ships the helper but it's a no-op until the
66
- // unified logger lands.
64
+ // Attach a QueueEvents listener that emits `job.completed`, `job.failed`,
65
+ // and `job.stalled` to the unified logger. Called once per (queue, events)
66
+ // pair, typically right after both are constructed.
67
+ //
68
+ // The Queue handle is required because BullMQ's QueueEvents payload doesn't
69
+ // include duration, attempts, or job name on its own — we look those up via
70
+ // queue.getJob( jobId ). The lookup is best-effort: jobs that have already
71
+ // been swept by removeOnComplete / removeOnFail return null, in which case
72
+ // the emit goes out with the fields we do have.
73
+ //
74
+ // duration is measured from worker pickup (processedOn) to finish
75
+ // (finishedOn), i.e. execution time. Queue-wait time (timestamp to
76
+ // processedOn) is not included; if that becomes interesting it can be
77
+ // added as a separate field.
78
+
79
+ const attachQueueEventsLogger = ( queue, queueEvents, logger ) => {
80
+
81
+ if( ! logger?.emit ) return;
82
+
83
+ const queueName = queue?.name;
84
+
85
+ queueEvents.on( 'completed', async ({ jobId }) => {
86
+
87
+ try {
67
88
 
68
- const attachQueueEventsLogger = ( queueEvents, queueName, logger ) => {
89
+ const job = await queue.getJob( jobId ).catch( () => null );
69
90
 
70
- if( ! logger?.event ) return;
91
+ const duration = ( job?.finishedOn && job?.processedOn )
92
+ ? job.finishedOn - job.processedOn
93
+ : null;
71
94
 
72
- queueEvents.on( 'completed', ({ jobId, returnvalue, prev }) => {
95
+ await logger.emit( 'job.completed', {
96
+ jobId,
97
+ queue : queueName,
98
+ name : job?.name,
99
+ duration,
100
+ attemptsMade : job?.attemptsMade
101
+ });
73
102
 
74
- logger.event( 'job.completed', {
75
- jobId,
76
- queue : queueName,
77
- prev
78
- });
103
+ } catch {}
79
104
 
80
105
  });
81
106
 
82
- queueEvents.on( 'failed', ({ jobId, failedReason, prev }) => {
107
+ queueEvents.on( 'failed', async ({ jobId, failedReason }) => {
83
108
 
84
- logger.event( 'job.failed', {
85
- jobId,
86
- queue : queueName,
87
- failedReason,
88
- prev
89
- });
109
+ try {
110
+
111
+ const job = await queue.getJob( jobId ).catch( () => null );
112
+
113
+ await logger.emit( 'job.failed', {
114
+ jobId,
115
+ queue : queueName,
116
+ name : job?.name,
117
+ failedReason,
118
+ attemptsMade : job?.attemptsMade
119
+ });
120
+
121
+ } catch {}
90
122
 
91
123
  });
92
124
 
93
- queueEvents.on( 'stalled', ({ jobId }) => {
125
+ queueEvents.on( 'stalled', async ({ jobId }) => {
126
+
127
+ try {
128
+
129
+ const job = await queue.getJob( jobId ).catch( () => null );
130
+
131
+ await logger.emit( 'job.stalled', {
132
+ jobId,
133
+ queue : queueName,
134
+ name : job?.name
135
+ });
94
136
 
95
- logger.event( 'job.stalled', {
96
- jobId,
97
- queue : queueName
98
- });
137
+ } catch {}
99
138
 
100
139
  });
101
140
 
package/dist/bullmq.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  currentTraceId,
3
3
  withTraceScope
4
- } from "./chunk-O6EHU4U7.js";
4
+ } from "./chunk-CODZHH5I.js";
5
5
 
6
6
  // bullmq.js
7
7
  import { randomUUID } from "crypto";
@@ -31,28 +31,46 @@ var enqueueFromWorker = async (queue, name, data, options = {}) => {
31
31
  options
32
32
  );
33
33
  };
34
- var attachQueueEventsLogger = (queueEvents, queueName, logger) => {
35
- if (!(logger == null ? void 0 : logger.event)) return;
36
- queueEvents.on("completed", ({ jobId, returnvalue, prev }) => {
37
- logger.event("job.completed", {
38
- jobId,
39
- queue: queueName,
40
- prev
41
- });
34
+ var attachQueueEventsLogger = (queue, queueEvents, logger) => {
35
+ if (!(logger == null ? void 0 : logger.emit)) return;
36
+ const queueName = queue == null ? void 0 : queue.name;
37
+ queueEvents.on("completed", async ({ jobId }) => {
38
+ try {
39
+ const job = await queue.getJob(jobId).catch(() => null);
40
+ const duration = (job == null ? void 0 : job.finishedOn) && (job == null ? void 0 : job.processedOn) ? job.finishedOn - job.processedOn : null;
41
+ await logger.emit("job.completed", {
42
+ jobId,
43
+ queue: queueName,
44
+ name: job == null ? void 0 : job.name,
45
+ duration,
46
+ attemptsMade: job == null ? void 0 : job.attemptsMade
47
+ });
48
+ } catch {
49
+ }
42
50
  });
43
- queueEvents.on("failed", ({ jobId, failedReason, prev }) => {
44
- logger.event("job.failed", {
45
- jobId,
46
- queue: queueName,
47
- failedReason,
48
- prev
49
- });
51
+ queueEvents.on("failed", async ({ jobId, failedReason }) => {
52
+ try {
53
+ const job = await queue.getJob(jobId).catch(() => null);
54
+ await logger.emit("job.failed", {
55
+ jobId,
56
+ queue: queueName,
57
+ name: job == null ? void 0 : job.name,
58
+ failedReason,
59
+ attemptsMade: job == null ? void 0 : job.attemptsMade
60
+ });
61
+ } catch {
62
+ }
50
63
  });
51
- queueEvents.on("stalled", ({ jobId }) => {
52
- logger.event("job.stalled", {
53
- jobId,
54
- queue: queueName
55
- });
64
+ queueEvents.on("stalled", async ({ jobId }) => {
65
+ try {
66
+ const job = await queue.getJob(jobId).catch(() => null);
67
+ await logger.emit("job.stalled", {
68
+ jobId,
69
+ queue: queueName,
70
+ name: job == null ? void 0 : job.name
71
+ });
72
+ } catch {
73
+ }
56
74
  });
57
75
  };
58
76
  export {
@@ -1,16 +1,72 @@
1
1
  // index.js
2
2
  import * as Sentry from "@sentry/node";
3
+ import pino from "pino";
3
4
  var init2 = (config = {}) => Sentry.init({
4
5
  dsn: process.env.SENTRY_DSN,
5
6
  enableLogs: true,
6
7
  environment: process.env.SENTRY_ENVIRONMENT || "localhost",
7
8
  tracesSampleRate: 0.1,
8
9
  profilesSampleRate: 0.05,
9
- ...config
10
+ ...config,
11
+ integrations: [
12
+ Sentry.pinoIntegration(),
13
+ ...config.integrations || []
14
+ ]
10
15
  });
11
16
  var captureException2 = (error, options) => Sentry.captureException(error, options);
12
17
  var captureMessage2 = (message, options) => Sentry.captureMessage(message, options);
13
18
  var logger2 = Sentry.logger;
19
+ var createLogger = ({ eventWriter, level = "info" } = {}) => {
20
+ const isProd = process.env.SENTRY_ENVIRONMENT === "production";
21
+ const pinoConfig = isProd ? { level } : {
22
+ level,
23
+ transport: {
24
+ target: "pino-pretty",
25
+ options: {
26
+ colorize: true,
27
+ translateTime: "SYS:standard"
28
+ }
29
+ }
30
+ };
31
+ const log = pino(pinoConfig);
32
+ const info = (message, data = {}) => log.info(data, message);
33
+ const warn = (message, data = {}) => log.warn(data, message);
34
+ const error = (first, second) => {
35
+ if (first instanceof Error) {
36
+ Sentry.captureException(first, second);
37
+ log.error(
38
+ { err: first, ...second },
39
+ first.message
40
+ );
41
+ } else if ((second == null ? void 0 : second.err) instanceof Error) {
42
+ Sentry.captureException(second.err);
43
+ log.error(second, first);
44
+ } else {
45
+ log.error(second || {}, first);
46
+ }
47
+ };
48
+ const emit = async (name, data = {}) => {
49
+ var _a, _b, _c, _d;
50
+ const scope = Sentry.getIsolationScope();
51
+ const scopeData = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
52
+ const traceId = (_b = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _b.traceId;
53
+ const userId = (_c = scopeData == null ? void 0 : scopeData.user) == null ? void 0 : _c.id;
54
+ const organizationId = (_d = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _d.organization;
55
+ log.info(
56
+ { event: name, traceId, userId, organizationId, ...data },
57
+ "event:" + name
58
+ );
59
+ if (eventWriter) {
60
+ await eventWriter(name, {
61
+ traceId,
62
+ userId,
63
+ organizationId,
64
+ data
65
+ });
66
+ }
67
+ };
68
+ return { info, warn, error, emit };
69
+ };
14
70
  var attached = false;
15
71
  var attachProcessHandlers = () => {
16
72
  if (attached) return;
@@ -60,6 +116,7 @@ export {
60
116
  captureException2 as captureException,
61
117
  captureMessage2 as captureMessage,
62
118
  logger2 as logger,
119
+ createLogger,
63
120
  attachProcessHandlers,
64
121
  withTraceScope,
65
122
  currentTraceId
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  attachProcessHandlers: () => attachProcessHandlers,
34
34
  captureException: () => captureException2,
35
35
  captureMessage: () => captureMessage2,
36
+ createLogger: () => createLogger,
36
37
  currentTraceId: () => currentTraceId,
37
38
  init: () => init2,
38
39
  logger: () => logger2,
@@ -40,17 +41,73 @@ __export(index_exports, {
40
41
  });
41
42
  module.exports = __toCommonJS(index_exports);
42
43
  var Sentry = __toESM(require("@sentry/node"), 1);
44
+ var import_pino = __toESM(require("pino"), 1);
43
45
  var init2 = (config = {}) => Sentry.init({
44
46
  dsn: process.env.SENTRY_DSN,
45
47
  enableLogs: true,
46
48
  environment: process.env.SENTRY_ENVIRONMENT || "localhost",
47
49
  tracesSampleRate: 0.1,
48
50
  profilesSampleRate: 0.05,
49
- ...config
51
+ ...config,
52
+ integrations: [
53
+ Sentry.pinoIntegration(),
54
+ ...config.integrations || []
55
+ ]
50
56
  });
51
57
  var captureException2 = (error, options) => Sentry.captureException(error, options);
52
58
  var captureMessage2 = (message, options) => Sentry.captureMessage(message, options);
53
59
  var logger2 = Sentry.logger;
60
+ var createLogger = ({ eventWriter, level = "info" } = {}) => {
61
+ const isProd = process.env.SENTRY_ENVIRONMENT === "production";
62
+ const pinoConfig = isProd ? { level } : {
63
+ level,
64
+ transport: {
65
+ target: "pino-pretty",
66
+ options: {
67
+ colorize: true,
68
+ translateTime: "SYS:standard"
69
+ }
70
+ }
71
+ };
72
+ const log = (0, import_pino.default)(pinoConfig);
73
+ const info = (message, data = {}) => log.info(data, message);
74
+ const warn = (message, data = {}) => log.warn(data, message);
75
+ const error = (first, second) => {
76
+ if (first instanceof Error) {
77
+ Sentry.captureException(first, second);
78
+ log.error(
79
+ { err: first, ...second },
80
+ first.message
81
+ );
82
+ } else if ((second == null ? void 0 : second.err) instanceof Error) {
83
+ Sentry.captureException(second.err);
84
+ log.error(second, first);
85
+ } else {
86
+ log.error(second || {}, first);
87
+ }
88
+ };
89
+ const emit = async (name, data = {}) => {
90
+ var _a, _b, _c, _d;
91
+ const scope = Sentry.getIsolationScope();
92
+ const scopeData = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
93
+ const traceId = (_b = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _b.traceId;
94
+ const userId = (_c = scopeData == null ? void 0 : scopeData.user) == null ? void 0 : _c.id;
95
+ const organizationId = (_d = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _d.organization;
96
+ log.info(
97
+ { event: name, traceId, userId, organizationId, ...data },
98
+ "event:" + name
99
+ );
100
+ if (eventWriter) {
101
+ await eventWriter(name, {
102
+ traceId,
103
+ userId,
104
+ organizationId,
105
+ data
106
+ });
107
+ }
108
+ };
109
+ return { info, warn, error, emit };
110
+ };
54
111
  var attached = false;
55
112
  var attachProcessHandlers = () => {
56
113
  if (attached) return;
@@ -99,6 +156,7 @@ var currentTraceId = () => {
99
156
  attachProcessHandlers,
100
157
  captureException,
101
158
  captureMessage,
159
+ createLogger,
102
160
  currentTraceId,
103
161
  init,
104
162
  logger,
package/dist/index.d.cts CHANGED
@@ -1,12 +1,18 @@
1
1
  import * as Sentry from '@sentry/node';
2
2
  export { Sentry };
3
+ import pino from 'pino';
3
4
 
4
5
  // Project-standard Sentry.init wrapper. Sets the conventions used across
5
6
  // drawbridge-api / -sync / -webhooks (enableLogs, tracesSampleRate,
6
- // profilesSampleRate, environment) so each service's init call collapses
7
- // to dsn + any overrides.
7
+ // profilesSampleRate, environment, pinoIntegration) so each service's init
8
+ // call collapses to dsn + any overrides.
8
9
  //
9
- // Defaults are conservative. Override per service as needed.
10
+ // pinoIntegration() ships with @sentry/node >=10.18 captures pino logger
11
+ // output as Sentry Logs with the active scope tags attached. Required for
12
+ // createLogger() to flow through to Sentry.
13
+ //
14
+ // Defaults are conservative. Override per service as needed. Consumer-passed
15
+ // integrations append to ours rather than replacing them.
10
16
 
11
17
  const init = ( config = {} ) => Sentry.init({
12
18
  dsn : process.env.SENTRY_DSN,
@@ -14,7 +20,11 @@ const init = ( config = {} ) => Sentry.init({
14
20
  environment : process.env.SENTRY_ENVIRONMENT || 'localhost',
15
21
  tracesSampleRate : 0.1,
16
22
  profilesSampleRate : 0.05,
17
- ...config
23
+ ...config,
24
+ integrations : [
25
+ Sentry.pinoIntegration(),
26
+ ...( config.integrations || [] )
27
+ ]
18
28
  });
19
29
 
20
30
  // Named re-exports of the Sentry surface services actually use. Keep this
@@ -24,11 +34,113 @@ const captureException = ( error, options ) => Sentry.captureException( error, o
24
34
 
25
35
  const captureMessage = ( message, options ) => Sentry.captureMessage( message, options );
26
36
 
27
- // Sentry.logger is the structured-logs surface (ships to Sentry Logs).
28
- // Exposed as `logger` so callers do `logger.info(...)` / `logger.error(...)`.
37
+ // Sentry.logger is the raw structured-logs surface (ships to Sentry Logs).
38
+ // Exposed as `logger` for one-off `logger.info(...)` / `logger.error(...)`
39
+ // without instantiating a per-service Pino instance. Prefer createLogger()
40
+ // in service code — it adds pretty local stdout, Sentry.captureException
41
+ // dispatch for Error-typed args, and the .event() method for writing
42
+ // domain events to a service-provided eventWriter (Mongo telemetry
43
+ // collection in drawbridge-api).
29
44
 
30
45
  const logger = Sentry.logger;
31
46
 
47
+ // Build a per-service logger. Pino does the local write (pretty stdout in
48
+ // non-production, JSON in production); Sentry.pinoIntegration in init()
49
+ // auto-captures the same lines as Sentry Logs with active-scope tags
50
+ // (user / organization / traceId / route / method) already attached.
51
+ //
52
+ // info / warn – structured log line
53
+ // error – log line; if first arg is Error (or `data.err` is),
54
+ // also calls Sentry.captureException so the throw lands
55
+ // in Sentry Issues with the same scope context
56
+ // emit(n, d) – emits a Pino info line tagged `event:<n>`, then calls
57
+ // eventWriter( name, { traceId, userId, organizationId,
58
+ // data }) so the caller can persist to its store
59
+ // (drawbridge-api wraps controller.create with the
60
+ // `telemetry` collection)
61
+ //
62
+ // Reads traceId / userId / organizationId from the active Sentry isolation
63
+ // scope so call sites pass only the event-specific payload.
64
+
65
+ const createLogger = ({ eventWriter, level = 'info' } = {}) => {
66
+
67
+ const isProd = process.env.SENTRY_ENVIRONMENT === 'production';
68
+
69
+ const pinoConfig = isProd
70
+ ? { level }
71
+ : {
72
+ level,
73
+ transport : {
74
+ target : 'pino-pretty',
75
+ options : {
76
+ colorize : true,
77
+ translateTime : 'SYS:standard'
78
+ }
79
+ }
80
+ };
81
+
82
+ const log = pino( pinoConfig );
83
+
84
+ const info = ( message, data = {} ) => log.info( data, message );
85
+
86
+ const warn = ( message, data = {} ) => log.warn( data, message );
87
+
88
+ const error = ( first, second ) => {
89
+
90
+ if( first instanceof Error ){
91
+
92
+ Sentry.captureException( first, second );
93
+
94
+ log.error(
95
+ { err : first, ...second },
96
+ first.message
97
+ );
98
+
99
+ } else if( second?.err instanceof Error ){
100
+
101
+ Sentry.captureException( second.err );
102
+
103
+ log.error( second, first );
104
+
105
+ } else {
106
+
107
+ log.error( second || {}, first );
108
+
109
+ }
110
+
111
+ };
112
+
113
+ const emit = async ( name, data = {} ) => {
114
+
115
+ const scope = Sentry.getIsolationScope();
116
+ const scopeData = scope?.getScopeData?.();
117
+
118
+ const traceId = scopeData?.tags?.traceId;
119
+ const userId = scopeData?.user?.id;
120
+ const organizationId = scopeData?.tags?.organization;
121
+
122
+ log.info(
123
+ { event : name, traceId, userId, organizationId, ...data },
124
+ 'event:' + name
125
+ );
126
+
127
+ if( eventWriter ){
128
+
129
+ await eventWriter( name, {
130
+ traceId,
131
+ userId,
132
+ organizationId,
133
+ data
134
+ });
135
+
136
+ }
137
+
138
+ };
139
+
140
+ return { info, warn, error, emit };
141
+
142
+ };
143
+
32
144
  // Diagnostic process-level handlers. Surfaces unhandled async errors to
33
145
  // Sentry (without these, modern Node defaults silently exit) and logs
34
146
  // exit-path signals to stderr so an operator can diagnose crashes.
@@ -119,4 +231,4 @@ const currentTraceId = () => {
119
231
 
120
232
  };
121
233
 
122
- export { attachProcessHandlers, captureException, captureMessage, currentTraceId, init, logger, withTraceScope };
234
+ export { attachProcessHandlers, captureException, captureMessage, createLogger, currentTraceId, init, logger, withTraceScope };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import * as Sentry from '@sentry/node';
2
2
  export { Sentry };
3
+ import pino from 'pino';
3
4
 
4
5
  // Project-standard Sentry.init wrapper. Sets the conventions used across
5
6
  // drawbridge-api / -sync / -webhooks (enableLogs, tracesSampleRate,
6
- // profilesSampleRate, environment) so each service's init call collapses
7
- // to dsn + any overrides.
7
+ // profilesSampleRate, environment, pinoIntegration) so each service's init
8
+ // call collapses to dsn + any overrides.
8
9
  //
9
- // Defaults are conservative. Override per service as needed.
10
+ // pinoIntegration() ships with @sentry/node >=10.18 captures pino logger
11
+ // output as Sentry Logs with the active scope tags attached. Required for
12
+ // createLogger() to flow through to Sentry.
13
+ //
14
+ // Defaults are conservative. Override per service as needed. Consumer-passed
15
+ // integrations append to ours rather than replacing them.
10
16
 
11
17
  const init = ( config = {} ) => Sentry.init({
12
18
  dsn : process.env.SENTRY_DSN,
@@ -14,7 +20,11 @@ const init = ( config = {} ) => Sentry.init({
14
20
  environment : process.env.SENTRY_ENVIRONMENT || 'localhost',
15
21
  tracesSampleRate : 0.1,
16
22
  profilesSampleRate : 0.05,
17
- ...config
23
+ ...config,
24
+ integrations : [
25
+ Sentry.pinoIntegration(),
26
+ ...( config.integrations || [] )
27
+ ]
18
28
  });
19
29
 
20
30
  // Named re-exports of the Sentry surface services actually use. Keep this
@@ -24,11 +34,113 @@ const captureException = ( error, options ) => Sentry.captureException( error, o
24
34
 
25
35
  const captureMessage = ( message, options ) => Sentry.captureMessage( message, options );
26
36
 
27
- // Sentry.logger is the structured-logs surface (ships to Sentry Logs).
28
- // Exposed as `logger` so callers do `logger.info(...)` / `logger.error(...)`.
37
+ // Sentry.logger is the raw structured-logs surface (ships to Sentry Logs).
38
+ // Exposed as `logger` for one-off `logger.info(...)` / `logger.error(...)`
39
+ // without instantiating a per-service Pino instance. Prefer createLogger()
40
+ // in service code — it adds pretty local stdout, Sentry.captureException
41
+ // dispatch for Error-typed args, and the .event() method for writing
42
+ // domain events to a service-provided eventWriter (Mongo telemetry
43
+ // collection in drawbridge-api).
29
44
 
30
45
  const logger = Sentry.logger;
31
46
 
47
+ // Build a per-service logger. Pino does the local write (pretty stdout in
48
+ // non-production, JSON in production); Sentry.pinoIntegration in init()
49
+ // auto-captures the same lines as Sentry Logs with active-scope tags
50
+ // (user / organization / traceId / route / method) already attached.
51
+ //
52
+ // info / warn – structured log line
53
+ // error – log line; if first arg is Error (or `data.err` is),
54
+ // also calls Sentry.captureException so the throw lands
55
+ // in Sentry Issues with the same scope context
56
+ // emit(n, d) – emits a Pino info line tagged `event:<n>`, then calls
57
+ // eventWriter( name, { traceId, userId, organizationId,
58
+ // data }) so the caller can persist to its store
59
+ // (drawbridge-api wraps controller.create with the
60
+ // `telemetry` collection)
61
+ //
62
+ // Reads traceId / userId / organizationId from the active Sentry isolation
63
+ // scope so call sites pass only the event-specific payload.
64
+
65
+ const createLogger = ({ eventWriter, level = 'info' } = {}) => {
66
+
67
+ const isProd = process.env.SENTRY_ENVIRONMENT === 'production';
68
+
69
+ const pinoConfig = isProd
70
+ ? { level }
71
+ : {
72
+ level,
73
+ transport : {
74
+ target : 'pino-pretty',
75
+ options : {
76
+ colorize : true,
77
+ translateTime : 'SYS:standard'
78
+ }
79
+ }
80
+ };
81
+
82
+ const log = pino( pinoConfig );
83
+
84
+ const info = ( message, data = {} ) => log.info( data, message );
85
+
86
+ const warn = ( message, data = {} ) => log.warn( data, message );
87
+
88
+ const error = ( first, second ) => {
89
+
90
+ if( first instanceof Error ){
91
+
92
+ Sentry.captureException( first, second );
93
+
94
+ log.error(
95
+ { err : first, ...second },
96
+ first.message
97
+ );
98
+
99
+ } else if( second?.err instanceof Error ){
100
+
101
+ Sentry.captureException( second.err );
102
+
103
+ log.error( second, first );
104
+
105
+ } else {
106
+
107
+ log.error( second || {}, first );
108
+
109
+ }
110
+
111
+ };
112
+
113
+ const emit = async ( name, data = {} ) => {
114
+
115
+ const scope = Sentry.getIsolationScope();
116
+ const scopeData = scope?.getScopeData?.();
117
+
118
+ const traceId = scopeData?.tags?.traceId;
119
+ const userId = scopeData?.user?.id;
120
+ const organizationId = scopeData?.tags?.organization;
121
+
122
+ log.info(
123
+ { event : name, traceId, userId, organizationId, ...data },
124
+ 'event:' + name
125
+ );
126
+
127
+ if( eventWriter ){
128
+
129
+ await eventWriter( name, {
130
+ traceId,
131
+ userId,
132
+ organizationId,
133
+ data
134
+ });
135
+
136
+ }
137
+
138
+ };
139
+
140
+ return { info, warn, error, emit };
141
+
142
+ };
143
+
32
144
  // Diagnostic process-level handlers. Surfaces unhandled async errors to
33
145
  // Sentry (without these, modern Node defaults silently exit) and logs
34
146
  // exit-path signals to stderr so an operator can diagnose crashes.
@@ -119,4 +231,4 @@ const currentTraceId = () => {
119
231
 
120
232
  };
121
233
 
122
- export { attachProcessHandlers, captureException, captureMessage, currentTraceId, init, logger, withTraceScope };
234
+ export { attachProcessHandlers, captureException, captureMessage, createLogger, currentTraceId, init, logger, withTraceScope };
package/dist/index.js CHANGED
@@ -3,16 +3,18 @@ import {
3
3
  attachProcessHandlers,
4
4
  captureException,
5
5
  captureMessage,
6
+ createLogger,
6
7
  currentTraceId,
7
8
  init,
8
9
  logger,
9
10
  withTraceScope
10
- } from "./chunk-O6EHU4U7.js";
11
+ } from "./chunk-CODZHH5I.js";
11
12
  export {
12
13
  Sentry,
13
14
  attachProcessHandlers,
14
15
  captureException,
15
16
  captureMessage,
17
+ createLogger,
16
18
  currentTraceId,
17
19
  init,
18
20
  logger,
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "type": "module",
3
3
  "dependencies": {
4
- "@drawbridge/drawbridge-agents": "0.0.4",
5
- "@sentry/node": "10.27.0"
4
+ "@drawbridge/drawbridge-agents": "0.0.5",
5
+ "@sentry/node": "10.27.0",
6
+ "pino": "10.3.1",
7
+ "pino-pretty": "13.1.3"
6
8
  },
7
9
  "peerDependencies": {
8
10
  "@sentry/nextjs": ">=10"
@@ -58,5 +60,5 @@
58
60
  "build": "tsup && npm publish"
59
61
  },
60
62
  "types": "dist/index.d.ts",
61
- "version": "0.0.6"
63
+ "version": "0.0.8"
62
64
  }