@monocle.sh/adonisjs-agent 1.0.0-beta.12 → 1.0.0-beta.14

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.
@@ -9,7 +9,6 @@ import { extractTraceContext, getCurrentSpan, injectTraceContext, otelLoggingPre
9
9
  * to extract source context lines from the stack trace.
10
10
  */
11
11
  declare function handleError(span: Span, error: Error): Promise<never>;
12
- type RecordCallback<T> = (span: Span) => T | Promise<T>;
13
12
  /**
14
13
  * Record a code section as a span in your traces.
15
14
  *
@@ -17,8 +16,9 @@ type RecordCallback<T> = (span: Span) => T | Promise<T>;
17
16
  * to capture exceptions with source context lines (pre/post context around
18
17
  * the error location).
19
18
  *
20
- * Always returns a Promise to ensure context lines are properly captured
21
- * even for synchronous errors.
19
+ * Preserves sync/async semantics: if the callback is synchronous, the return
20
+ * type is `T`. If the callback returns a Promise-like value, the return type
21
+ * is async-compatible.
22
22
  *
23
23
  * Automatically handles:
24
24
  * - Creating and closing the span
@@ -29,10 +29,10 @@ type RecordCallback<T> = (span: Span) => T | Promise<T>;
29
29
  * ```ts
30
30
  * import { record } from '@monocle.sh/adonisjs-agent'
31
31
  *
32
- * const result = await record('database.query', () => {
33
- * return db.query('SELECT * FROM users')
34
- * })
32
+ * // Sync callback returns T directly
33
+ * const value = record('compute.hash', () => computeHash(data))
35
34
  *
35
+ * // Async callback returns Promise<T>
36
36
  * const user = await record('user.fetch', async () => {
37
37
  * return await userService.findById(id)
38
38
  * })
@@ -44,6 +44,6 @@ type RecordCallback<T> = (span: Span) => T | Promise<T>;
44
44
  * })
45
45
  * ```
46
46
  */
47
- declare function record<T>(name: string, callback: RecordCallback<T>): Promise<T>;
47
+ declare function record<T>(name: string, callback: (span: Span) => T): T;
48
48
  //#endregion
49
49
  export { extractTraceContext, getCurrentSpan, handleError, injectTraceContext, otelLoggingPreset, record, recordEvent, setAttributes };
package/dist/helpers.mjs CHANGED
@@ -1,8 +1,31 @@
1
1
  import { ExceptionReporter } from "./src/exception_reporter.mjs";
2
- import { SpanStatusCode, trace } from "@opentelemetry/api";
2
+ import { trace } from "@opentelemetry/api";
3
3
  import { extractTraceContext, getCurrentSpan, injectTraceContext, otelLoggingPreset, recordEvent, setAttributes } from "@adonisjs/otel/helpers";
4
4
 
5
5
  //#region src/helpers.ts
6
+ function isPromiseLike(value) {
7
+ if (!value) return false;
8
+ if (typeof value !== "object" && typeof value !== "function") return false;
9
+ return typeof value.then === "function";
10
+ }
11
+ async function reportErrorAndEndSpan(span, error, endTime) {
12
+ const reporter = new ExceptionReporter();
13
+ try {
14
+ await reporter.report({
15
+ span,
16
+ error,
17
+ shouldReport: true
18
+ });
19
+ } catch {
20
+ reporter.reportSync({
21
+ span,
22
+ error,
23
+ shouldReport: true
24
+ });
25
+ } finally {
26
+ span.end(endTime);
27
+ }
28
+ }
6
29
  /**
7
30
  * Handle an error by reporting it with context frames extraction.
8
31
  *
@@ -10,16 +33,7 @@ import { extractTraceContext, getCurrentSpan, injectTraceContext, otelLoggingPre
10
33
  * to extract source context lines from the stack trace.
11
34
  */
12
35
  async function handleError(span, error) {
13
- await new ExceptionReporter().report({
14
- span,
15
- error,
16
- shouldReport: true
17
- });
18
- span.setStatus({
19
- code: SpanStatusCode.ERROR,
20
- message: error?.message
21
- });
22
- span.end();
36
+ await reportErrorAndEndSpan(span, error, /* @__PURE__ */ new Date());
23
37
  throw error;
24
38
  }
25
39
  /**
@@ -29,8 +43,9 @@ async function handleError(span, error) {
29
43
  * to capture exceptions with source context lines (pre/post context around
30
44
  * the error location).
31
45
  *
32
- * Always returns a Promise to ensure context lines are properly captured
33
- * even for synchronous errors.
46
+ * Preserves sync/async semantics: if the callback is synchronous, the return
47
+ * type is `T`. If the callback returns a Promise-like value, the return type
48
+ * is async-compatible.
34
49
  *
35
50
  * Automatically handles:
36
51
  * - Creating and closing the span
@@ -41,10 +56,10 @@ async function handleError(span, error) {
41
56
  * ```ts
42
57
  * import { record } from '@monocle.sh/adonisjs-agent'
43
58
  *
44
- * const result = await record('database.query', () => {
45
- * return db.query('SELECT * FROM users')
46
- * })
59
+ * // Sync callback returns T directly
60
+ * const value = record('compute.hash', () => computeHash(data))
47
61
  *
62
+ * // Async callback returns Promise<T>
48
63
  * const user = await record('user.fetch', async () => {
49
64
  * return await userService.findById(id)
50
65
  * })
@@ -56,14 +71,19 @@ async function handleError(span, error) {
56
71
  * })
57
72
  * ```
58
73
  */
59
- async function record(name, callback) {
60
- return trace.getTracer("@monocle.sh/agent").startActiveSpan(name, async (span) => {
74
+ function record(name, callback) {
75
+ return trace.getTracer("@monocle.sh/agent").startActiveSpan(name, (span) => {
61
76
  try {
62
- const result = await callback(span);
77
+ const result = callback(span);
78
+ if (isPromiseLike(result)) return Promise.resolve(result).then((value) => {
79
+ span.end();
80
+ return value;
81
+ }).catch((error) => handleError(span, error));
63
82
  span.end();
64
83
  return result;
65
84
  } catch (error) {
66
- return handleError(span, error);
85
+ reportErrorAndEndSpan(span, error, /* @__PURE__ */ new Date());
86
+ throw error;
67
87
  }
68
88
  });
69
89
  }
package/dist/init.mjs CHANGED
@@ -74,6 +74,20 @@ async function init(dirname) {
74
74
  const { instrumentMail } = await import("./src/instrumentations/mail/instrumentation.mjs");
75
75
  await instrumentMail(mailConfig ?? { enabled: true }, dirname);
76
76
  }
77
+ /**
78
+ * Cache Instrumentation
79
+ *
80
+ * Automatically instruments @adonisjs/cache (bentocache) if installed.
81
+ * Creates spans for cache operations (get, set, getOrSet, delete, etc.)
82
+ * and suppresses internal Redis/L2/bus spans.
83
+ *
84
+ * @see ./instrumentations/cache/instrumentation.ts for implementation details
85
+ */
86
+ const cacheConfig = config.cache;
87
+ if (cacheConfig !== false) {
88
+ const { instrumentCache } = await import("./src/instrumentations/cache/instrumentation.mjs");
89
+ await instrumentCache(cacheConfig ?? { enabled: true }, dirname);
90
+ }
77
91
  await waitForAllMessagesAcknowledged();
78
92
  }
79
93
 
@@ -33,6 +33,23 @@ var ExceptionReporter = class {
33
33
  const attributes = this.#buildAttributes(error, causeChain);
34
34
  span.addEvent("exception", attributes);
35
35
  }
36
+ /**
37
+ * Synchronous version of report. Uses buildCauseChain (no file I/O)
38
+ * so source context frames won't be available, but the full stack trace,
39
+ * cause chain, error status, and exception event are still recorded.
40
+ */
41
+ reportSync(options) {
42
+ const { span, shouldReport } = options;
43
+ const error = toHttpError(options.error);
44
+ span.setStatus({
45
+ code: SpanStatusCode.ERROR,
46
+ message: error.message
47
+ });
48
+ span.setAttribute("monocle.exception.should_report", shouldReport);
49
+ const causeChain = buildCauseChain(error);
50
+ const attributes = this.#buildAttributes(error, causeChain);
51
+ span.addEvent("exception", attributes);
52
+ }
36
53
  #buildAttributes(error, causeChain) {
37
54
  const hasCauses = causeChain.length > 1;
38
55
  const attributes = {
@@ -0,0 +1,55 @@
1
+ import { createRequire } from "node:module";
2
+ import { pathToFileURL } from "node:url";
3
+ import { BentoCacheInstrumentation } from "@bentocache/otel";
4
+
5
+ //#region src/instrumentations/cache/instrumentation.ts
6
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
+ const numericPattern = /^\d+$/;
8
+ const hexPattern = /^[0-9a-f]{8,}$/i;
9
+ /**
10
+ * Replaces dynamic-looking segments (UUIDs, numeric IDs, hex hashes)
11
+ * with `*` to reduce cardinality in span attributes.
12
+ */
13
+ function defaultKeySanitizer(key) {
14
+ if (!key) return key;
15
+ return key.split(":").map((segment) => {
16
+ if (uuidPattern.test(segment)) return "*";
17
+ if (numericPattern.test(segment)) return "*";
18
+ if (hexPattern.test(segment)) return "*";
19
+ return segment;
20
+ }).join(":");
21
+ }
22
+ /**
23
+ * Auto-instruments @adonisjs/cache (bentocache) with OpenTelemetry spans
24
+ * for cache operations (get, set, getOrSet, delete, etc.).
25
+ * Internal Redis/L2/bus spans are suppressed via `suppressInternalOperations`.
26
+ */
27
+ async function instrumentCache(config, appRoot) {
28
+ if (config.enabled === false) return void 0;
29
+ const appRequire = createRequire(appRoot ? pathToFileURL(`${appRoot}/package.json`).href : import.meta.url);
30
+ try {
31
+ appRequire.resolve("@adonisjs/cache");
32
+ } catch {
33
+ return;
34
+ }
35
+ const instrumentation = new BentoCacheInstrumentation({
36
+ requireParentSpan: config.requireParentSpan ?? false,
37
+ includeKeys: config.includeKeys ?? true,
38
+ keySanitizer: config.keySanitizer ?? defaultKeySanitizer,
39
+ suppressInternalOperations: true
40
+ });
41
+ instrumentation.enable();
42
+ /**
43
+ * Eagerly import bentocache to trigger the ESM hook.
44
+ * If the hook didn't patch BentoCache (e.g. race condition),
45
+ * fall back to manual registration.
46
+ */
47
+ try {
48
+ const bentocache = await import("bentocache");
49
+ if (bentocache.BentoCache.name !== "BentoCachePatched") instrumentation.manuallyRegister(bentocache);
50
+ } catch {}
51
+ return instrumentation;
52
+ }
53
+
54
+ //#endregion
55
+ export { instrumentCache };
@@ -15,6 +15,14 @@ interface CaptureMessageContext extends CaptureContext {
15
15
  level?: MessageLevel;
16
16
  }
17
17
  type CaptureExceptionContext = CaptureContext;
18
+ /**
19
+ * User information for tracing. When displayed in the Monocle UI,
20
+ * `name` takes priority over `email` as the display label, with
21
+ * `email` used as fallback.
22
+ */
23
+ interface MonocleUser extends UserContextResult {
24
+ name?: string;
25
+ }
18
26
  /**
19
27
  * Monocle helper class for manual instrumentation.
20
28
  */
@@ -41,8 +49,10 @@ declare class Monocle {
41
49
  static captureMessage(message: string, levelOrContext?: MessageLevel | CaptureMessageContext): Promise<void>;
42
50
  /**
43
51
  * Set user information on the current active span.
52
+ * `name` is displayed with priority over `email` in the Monocle UI.
53
+ * Falls back to `email`, then `id`.
44
54
  */
45
- static setUser(user: UserContextResult): void;
55
+ static setUser(user: MonocleUser): void;
46
56
  }
47
57
  //#endregion
48
- export { Monocle };
58
+ export { Monocle, MonocleUser };
@@ -100,6 +100,8 @@ var Monocle = class {
100
100
  }
101
101
  /**
102
102
  * Set user information on the current active span.
103
+ * `name` is displayed with priority over `email` in the Monocle UI.
104
+ * Falls back to `email`, then `id`.
103
105
  */
104
106
  static setUser(user) {
105
107
  setUser(user);
@@ -1,6 +1,11 @@
1
1
  import { DestinationMap, OtelConfig } from "@adonisjs/otel/types";
2
+ import { BentoCacheInstrumentationConfig } from "@bentocache/otel/types";
2
3
 
3
4
  //#region src/types.d.ts
5
+ /**
6
+ * Configuration for cache instrumentation
7
+ */
8
+ interface CacheInstrumentationConfig extends BentoCacheInstrumentationConfig {}
4
9
  /**
5
10
  * Configuration for mail instrumentation
6
11
  */
@@ -131,6 +136,13 @@ interface MonocleConfig extends Omit<OtelConfig, 'traceExporter' | 'metricExport
131
136
  * @default { enabled: true }
132
137
  */
133
138
  mail?: false | MailInstrumentationConfig;
139
+ /**
140
+ * Cache instrumentation configuration.
141
+ * Automatically instruments @adonisjs/cache (bentocache) if installed.
142
+ * Set to `false` to disable.
143
+ * @default { enabled: true }
144
+ */
145
+ cache?: false | CacheInstrumentationConfig;
134
146
  }
135
147
  //#endregion
136
148
  export { BatchConfig, CliTracingConfig, HostMetricsConfig, MonocleConfig };
@@ -1,7 +1,7 @@
1
1
  {{{
2
2
  exports({ to: app.configPath('monocle.ts') })
3
3
  }}}
4
- import { defineConfig, destinations } from '@monocle.sh/adonisjs-agent'
4
+ import { defineConfig } from '@monocle.sh/adonisjs-agent'
5
5
  import env from '#start/env'
6
6
 
7
7
  export default defineConfig({
@@ -10,12 +10,4 @@ export default defineConfig({
10
10
  serviceName: env.get('APP_NAME'),
11
11
  serviceVersion: env.get('APP_VERSION'),
12
12
  environment: env.get('APP_ENV'),
13
-
14
- // Optional: Additional OTLP destinations (Monocle is injected automatically)
15
- // destinations: {
16
- // grafana: destinations.otlp({
17
- // endpoint: env.get('GRAFANA_OTLP_ENDPOINT'),
18
- // signals: 'all',
19
- // }),
20
- // },
21
13
  })
package/dist/types.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BatchConfig, CliTracingConfig, HostMetricsConfig, MonocleConfig } from "./src/types.mjs";
2
+ import { MonocleUser } from "./src/monocle.mjs";
2
3
  import { SpanAllOptions, SpanOptions } from "./decorators.mjs";
3
4
  import { DestinationConfig, DestinationMap, DestinationSignal, DestinationSignals, HeadersCarrier, OtelLoggingPresetOptions, OtlpDestinationConfig, OtlpDestinationOptions, UserContextResult } from "@adonisjs/otel/types";
4
- export { type BatchConfig, type CliTracingConfig, type DestinationConfig, type DestinationMap, type DestinationSignal, type DestinationSignals, type HeadersCarrier, type HostMetricsConfig, type MonocleConfig, type OtelLoggingPresetOptions, type OtlpDestinationConfig, type OtlpDestinationOptions, type SpanAllOptions, type SpanOptions, type UserContextResult };
5
+ export { type BatchConfig, type CliTracingConfig, type DestinationConfig, type DestinationMap, type DestinationSignal, type DestinationSignals, type HeadersCarrier, type HostMetricsConfig, type MonocleConfig, type MonocleUser, type OtelLoggingPresetOptions, type OtlpDestinationConfig, type OtlpDestinationOptions, type SpanAllOptions, type SpanOptions, type UserContextResult };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monocle.sh/adonisjs-agent",
3
- "version": "1.0.0-beta.12",
3
+ "version": "1.0.0-beta.14",
4
4
  "description": "Monocle agent for AdonisJS - sends telemetry to Monocle cloud",
5
5
  "keywords": [
6
6
  "adonisjs",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@adonisjs/otel": "^1.2.0",
35
+ "@bentocache/otel": "^0.1.2",
35
36
  "@opentelemetry/api": "^1.9.0",
36
37
  "@opentelemetry/core": "^2.5.0",
37
38
  "@opentelemetry/exporter-metrics-otlp-http": "^0.211.0",