@monocle.sh/adonisjs-agent 1.0.0-beta.4

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/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # @monocle.sh/adonisjs-agent
2
+
3
+ Monocle agent for AdonisJS - sends telemetry to Monocle cloud.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @monocle.sh/adonisjs-agent
9
+ # or
10
+ yarn add @monocle.sh/adonisjs-agent
11
+ # or
12
+ pnpm add @monocle.sh/adonisjs-agent
13
+ ```
14
+
15
+ ## Configuration
16
+
17
+ Run the configure command to set up the agent automatically:
18
+
19
+ ```bash
20
+ node ace configure @monocle.sh/adonisjs-agent
21
+ ```
22
+
23
+ This will:
24
+
25
+ 1. Create `config/monocle.ts` configuration file
26
+ 2. Create `otel.ts` initialization file at project root
27
+ 3. Add the otel.ts import as the first import in `bin/server.ts`
28
+ 4. Register the Monocle provider in `adonisrc.ts`
29
+ 5. Register the Monocle middleware as the first router middleware
30
+ 6. Add required environment variables to `.env` and `start/env.ts`
31
+
32
+ After configuration, add your API key to `.env`:
33
+
34
+ ```env
35
+ MONOCLE_API_KEY=mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
36
+ ```
37
+
38
+ ## User Identification
39
+
40
+ To associate telemetry data with authenticated users, add `Monocle.setUser()` in your authentication middleware:
41
+
42
+ ```typescript
43
+ import type { NextFn } from '@adonisjs/core/types/http'
44
+ import type { HttpContext } from '@adonisjs/core/http'
45
+ import { Monocle } from '@monocle.sh/adonisjs-agent'
46
+
47
+ export default class SilentAuthMiddleware {
48
+ async handle(ctx: HttpContext, next: NextFn) {
49
+ await ctx.auth.check()
50
+
51
+ if (ctx.auth.user) Monocle.setUser(ctx.auth.user)
52
+
53
+ return next()
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## Exception Tracking
59
+
60
+ Exceptions are automatically recorded in spans when thrown during a request. The agent hooks into AdonisJS's `ExceptionHandler.report()` method to capture exceptions with their stack traces.
61
+
62
+ If you want to manually capture exceptions in custom code:
63
+
64
+ ```typescript
65
+ import { Monocle } from '@monocle.sh/adonisjs-agent'
66
+
67
+ try {
68
+ // your code
69
+ } catch (error) {
70
+ Monocle.captureException(error, {
71
+ user: { id: '123', email: 'user@example.com' },
72
+ tags: { component: 'payment' },
73
+ extra: { orderId: 456 },
74
+ })
75
+ throw error
76
+ }
77
+ ```
78
+
79
+ ## Configuration Options
80
+
81
+ The `config/monocle.ts` file supports the following options:
82
+
83
+ ```typescript
84
+ import { defineConfig } from '@monocle.sh/adonisjs-agent'
85
+ import env from '#start/env'
86
+
87
+ export default defineConfig({
88
+ // Required: Your Monocle API key
89
+ apiKey: env.get('MONOCLE_API_KEY'),
90
+
91
+ // Optional: Custom ingestion endpoint (for development)
92
+ // endpoint: 'http://localhost:4318',
93
+
94
+ // Service identification
95
+ serviceName: env.get('APP_NAME'),
96
+ serviceVersion: env.get('APP_VERSION'),
97
+ environment: env.get('APP_ENV'),
98
+
99
+ // Host metrics (CPU, Memory, Network, etc.)
100
+ // Set to false to disable
101
+ hostMetrics: {
102
+ enabled: true,
103
+ },
104
+
105
+ // CLI command tracing
106
+ cli: {
107
+ enabled: false,
108
+ exclude: ['make:*', 'generate:*', 'queue:work', 'queue:listen'],
109
+ },
110
+
111
+ // Trace batching configuration
112
+ batch: {
113
+ maxExportBatchSize: 512,
114
+ scheduledDelayMillis: 5000,
115
+ },
116
+
117
+ // Enable gzip compression (default: true)
118
+ compression: true,
119
+ })
120
+ ```
121
+
122
+ ## Environment Variables
123
+
124
+ | Variable | Description | Required |
125
+ | ----------------- | ------------------------------------------------------ | -------- |
126
+ | `MONOCLE_API_KEY` | Your Monocle API key | Yes |
127
+ | `APP_NAME` | Service name for identification | Yes |
128
+ | `APP_VERSION` | Service version (e.g., git sha, semver) | Yes |
129
+ | `APP_ENV` | Environment: `development`, `staging`, or `production` | Yes |
130
+
131
+ ## License
132
+
133
+ ISC
@@ -0,0 +1,7 @@
1
+ import ConfigureCommand from "@adonisjs/core/commands/configure";
2
+
3
+ //#region configure.d.ts
4
+
5
+ declare function configure(command: ConfigureCommand): Promise<void>;
6
+ //#endregion
7
+ export { configure };
@@ -0,0 +1,61 @@
1
+ import { stubsRoot } from "./stubs/main.mjs";
2
+
3
+ //#region configure.ts
4
+ async function configure(command) {
5
+ const codemods = await command.createCodemods();
6
+ /**
7
+ * Publish config/monocle.ts
8
+ */
9
+ await codemods.makeUsingStub(stubsRoot, "config.stub", {});
10
+ /**
11
+ * Publish otel.ts at project root
12
+ */
13
+ await codemods.makeUsingStub(stubsRoot, "otel.stub", {});
14
+ const serverFile = (await codemods.getTsMorphProject())?.getSourceFile(command.app.makePath("bin/server.ts"));
15
+ if (serverFile) {
16
+ const insertIndex = serverFile.getImportDeclarations()[0]?.getChildIndex() ?? 0;
17
+ serverFile.insertStatements(insertIndex, [
18
+ "/**",
19
+ " * OpenTelemetry initialization - MUST be the first import",
20
+ " * @see https://opentelemetry.io/docs/languages/js/getting-started/nodejs/",
21
+ " */",
22
+ `import '../otel.js'`,
23
+ ""
24
+ ]);
25
+ await serverFile.save();
26
+ }
27
+ /**
28
+ * Register the provider in adonisrc.ts
29
+ */
30
+ await codemods.updateRcFile((rcFile) => {
31
+ rcFile.addProvider("@monocle.sh/adonisjs-agent/monocle_provider");
32
+ });
33
+ /**
34
+ * Register the middleware as FIRST router middleware
35
+ */
36
+ await codemods.registerMiddleware("router", [{
37
+ path: "@monocle.sh/adonisjs-agent/monocle_middleware",
38
+ position: "before"
39
+ }]);
40
+ /**
41
+ * Define environment variables in .env
42
+ */
43
+ await codemods.defineEnvVariables({
44
+ APP_NAME: command.app.appName,
45
+ APP_VERSION: "0.0.1",
46
+ APP_ENV: "development",
47
+ MONOCLE_API_KEY: ""
48
+ });
49
+ /**
50
+ * Define environment validations in start/env.ts
51
+ */
52
+ await codemods.defineEnvValidations({ variables: {
53
+ APP_NAME: "Env.schema.string()",
54
+ APP_VERSION: "Env.schema.string()",
55
+ APP_ENV: `Env.schema.enum(['development', 'staging', 'production'] as const)`,
56
+ MONOCLE_API_KEY: "Env.schema.string()"
57
+ } });
58
+ }
59
+
60
+ //#endregion
61
+ export { configure };
@@ -0,0 +1,5 @@
1
+ import { BatchConfig, CliTracingConfig, HostMetricsConfig, MonocleConfig } from "./src/types.mjs";
2
+ import { defineConfig } from "./src/define_config.mjs";
3
+ import { Monocle } from "./src/monocle.mjs";
4
+ import { configure } from "./configure.mjs";
5
+ export { type BatchConfig, type CliTracingConfig, type HostMetricsConfig, Monocle, type MonocleConfig, configure, defineConfig };
package/dist/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from "./src/define_config.mjs";
2
+ import { Monocle } from "./src/monocle.mjs";
3
+ import { configure } from "./configure.mjs";
4
+
5
+ export { Monocle, configure, defineConfig };
@@ -0,0 +1,8 @@
1
+ //#region src/init.d.ts
2
+ /**
3
+ * Also stolen and tweaked from @adonisjs/otel in order to use our own config file.
4
+ * Need to be able to remove this file in the future.
5
+ */
6
+ declare function init(dirname: string): Promise<void>;
7
+ //#endregion
8
+ export { init };
package/dist/init.mjs ADDED
@@ -0,0 +1,112 @@
1
+ import { register } from "node:module";
2
+ import { pathToFileURL } from "node:url";
3
+ import { join } from "node:path";
4
+ import { createAddHookMessageChannel } from "import-in-the-middle";
5
+
6
+ //#region src/init.ts
7
+ /**
8
+ * Also stolen and tweaked from @adonisjs/otel in order to use our own config file.
9
+ * Need to be able to remove this file in the future.
10
+ */
11
+ const DEFAULT_BATCH_CONFIG = {
12
+ maxExportBatchSize: 512,
13
+ scheduledDelayMillis: 5e3,
14
+ exportTimeoutMillis: 3e4,
15
+ maxQueueSize: 2048
16
+ };
17
+ async function loadConfig(path) {
18
+ return await import(pathToFileURL(path).href).then((mod) => mod.default || mod).catch((error) => {
19
+ throw new Error(`Failed to load Monocle config file at "${path}"`, { cause: error });
20
+ });
21
+ }
22
+ function setupHooks() {
23
+ const { registerOptions, waitForAllMessagesAcknowledged } = createAddHookMessageChannel();
24
+ register("import-in-the-middle/hook.mjs", import.meta.url, registerOptions);
25
+ return waitForAllMessagesAcknowledged;
26
+ }
27
+ /**
28
+ * Create a metric reader for exporting metrics via OTLP
29
+ */
30
+ async function createMetricReader(config) {
31
+ const { PeriodicExportingMetricReader } = await import("@opentelemetry/sdk-metrics");
32
+ const { OTLPMetricExporter } = await import("@opentelemetry/exporter-metrics-otlp-http");
33
+ return new PeriodicExportingMetricReader({
34
+ exporter: new OTLPMetricExporter({ compression: config.compression !== false ? "gzip" : void 0 }),
35
+ exportIntervalMillis: 6e4
36
+ });
37
+ }
38
+ /**
39
+ * Create a BatchSpanProcessor for efficient trace export with compression
40
+ */
41
+ async function createSpanProcessor(config) {
42
+ const { BatchSpanProcessor } = await import("@opentelemetry/sdk-trace-base");
43
+ const { OTLPTraceExporter } = await import("@opentelemetry/exporter-trace-otlp-http");
44
+ const compression = config.compression !== false ? "gzip" : void 0;
45
+ const batchConfig = {
46
+ ...DEFAULT_BATCH_CONFIG,
47
+ ...config.batch
48
+ };
49
+ return new BatchSpanProcessor(new OTLPTraceExporter({ compression }), {
50
+ maxExportBatchSize: batchConfig.maxExportBatchSize,
51
+ scheduledDelayMillis: batchConfig.scheduledDelayMillis,
52
+ exportTimeoutMillis: batchConfig.exportTimeoutMillis,
53
+ maxQueueSize: batchConfig.maxQueueSize
54
+ });
55
+ }
56
+ async function init(dirname$1) {
57
+ const waitForAllMessagesAcknowledged = setupHooks();
58
+ const { OtelManager } = await import("@adonisjs/otel/manager");
59
+ const config = await loadConfig(join(dirname$1, "config/monocle.js"));
60
+ if (!config) return;
61
+ if (!OtelManager.isEnabled(config)) return;
62
+ const metricReader = await createMetricReader(config);
63
+ const spanProcessor = await createSpanProcessor(config);
64
+ const configWithProcessors = {
65
+ ...config,
66
+ metricReader,
67
+ spanProcessors: [spanProcessor, ...config.spanProcessors || []]
68
+ };
69
+ const manager = OtelManager.create(configWithProcessors);
70
+ manager?.start();
71
+ const shutdown = async () => {
72
+ await manager?.shutdown().catch(() => {
73
+ console.error("Error during OTEL shutdown");
74
+ });
75
+ };
76
+ process.on("beforeExit", shutdown);
77
+ process.on("SIGINT", async () => {
78
+ await shutdown();
79
+ process.exit(0);
80
+ });
81
+ process.on("SIGTERM", async () => {
82
+ await shutdown();
83
+ process.exit(0);
84
+ });
85
+ const hostMetricsConfig = config.hostMetrics;
86
+ if (hostMetricsConfig !== false) {
87
+ const { startHostMetrics } = await import("./src/host_metrics.mjs");
88
+ startHostMetrics(hostMetricsConfig === void 0 ? {} : hostMetricsConfig);
89
+ }
90
+ /**
91
+ * CLI Command Instrumentation
92
+ *
93
+ * When enabled, this patches AdonisJS Ace commands to create OTEL spans.
94
+ * The `dirname` parameter is passed so the instrumentation can resolve
95
+ * @adonisjs/core/ace from the app's node_modules (pnpm isolation).
96
+ *
97
+ * Prerequisites for this to work:
98
+ * 1. App's `bin/console.ts` must import `otel.ts` FIRST (before reflect-metadata)
99
+ * 2. Config must have `cli.enabled: true`
100
+ *
101
+ * @see ./cli_instrumentation.ts for implementation details
102
+ */
103
+ const cliConfig = config.cli;
104
+ if (cliConfig !== false && cliConfig?.enabled) {
105
+ const { instrumentCliCommands } = await import("./src/cli_instrumentation.mjs");
106
+ await instrumentCliCommands(cliConfig, dirname$1);
107
+ }
108
+ await waitForAllMessagesAcknowledged();
109
+ }
110
+
111
+ //#endregion
112
+ export { init };
@@ -0,0 +1,2 @@
1
+ import OtelMiddleware from "@adonisjs/otel/otel_middleware";
2
+ export { OtelMiddleware as default };
@@ -0,0 +1,7 @@
1
+ import OtelMiddleware from "@adonisjs/otel/otel_middleware";
2
+
3
+ //#region middleware/monocle_middleware.ts
4
+ var monocle_middleware_default = OtelMiddleware;
5
+
6
+ //#endregion
7
+ export { monocle_middleware_default as default };
@@ -0,0 +1,20 @@
1
+ import { ApplicationService } from "@adonisjs/core/types";
2
+
3
+ //#region providers/monocle_provider.d.ts
4
+
5
+ /**
6
+ * Stolen from @Adonisjs/otel to use another config filename. we need to expose this
7
+ * option from @adonis/otel so we can remove this file in the future.
8
+ */
9
+ declare class OtelProvider {
10
+ #private;
11
+ protected app: ApplicationService;
12
+ constructor(app: ApplicationService);
13
+ register(): void;
14
+ /**
15
+ * Gracefully flush pending spans
16
+ */
17
+ shutdown(): Promise<void>;
18
+ }
19
+ //#endregion
20
+ export { OtelProvider as default };
@@ -0,0 +1,66 @@
1
+ import { SpanStatusCode } from "@opentelemetry/api";
2
+ import { getCurrentSpan } from "@adonisjs/otel/helpers";
3
+ import OtelMiddleware from "@adonisjs/otel/otel_middleware";
4
+ import { OtelManager } from "@adonisjs/otel";
5
+ import { ExceptionHandler } from "@adonisjs/core/http";
6
+ import { configProvider } from "@adonisjs/core";
7
+
8
+ //#region providers/monocle_provider.ts
9
+ /**
10
+ * Stolen from @Adonisjs/otel to use another config filename. we need to expose this
11
+ * option from @adonis/otel so we can remove this file in the future.
12
+ */
13
+ var OtelProvider = class {
14
+ constructor(app) {
15
+ this.app = app;
16
+ }
17
+ /**
18
+ * Hook into ExceptionHandler to record exceptions in spans.
19
+ *
20
+ * We always record the exception on the span (for trace visibility), but we also
21
+ * add an attribute `monocle.exception.should_report` to indicate whether this
22
+ * exception should appear in the Exceptions dashboard.
23
+ *
24
+ * This respects the AdonisJS `ignoreExceptions`, `ignoreStatuses`, and `ignoreCodes`
25
+ * configuration from the ExceptionHandler.
26
+ */
27
+ #registerExceptionHandler() {
28
+ const originalReport = ExceptionHandler.prototype.report;
29
+ ExceptionHandler.macro("report", async function(error, ctx) {
30
+ const span = getCurrentSpan();
31
+ if (span && error instanceof Error) {
32
+ span.recordException(error);
33
+ span.setStatus({
34
+ code: SpanStatusCode.ERROR,
35
+ message: error.message
36
+ });
37
+ const httpError = this.toHttpError(error);
38
+ const shouldReport = this.shouldReport(httpError);
39
+ span.setAttribute("monocle.exception.should_report", shouldReport);
40
+ }
41
+ return originalReport.call(this, error, ctx);
42
+ });
43
+ }
44
+ register() {
45
+ this.#registerExceptionHandler();
46
+ this.#registerMiddleware();
47
+ }
48
+ /**
49
+ * Register the OtelMiddleware as a singleton in the container
50
+ */
51
+ #registerMiddleware() {
52
+ this.app.container.singleton(OtelMiddleware, async () => {
53
+ const otelConfigProvider = this.app.config.get("monocle", {});
54
+ return new OtelMiddleware({ userContext: (await configProvider.resolve(this.app, otelConfigProvider))?.userContext });
55
+ });
56
+ }
57
+ /**
58
+ * Gracefully flush pending spans
59
+ */
60
+ async shutdown() {
61
+ await OtelManager.getInstance()?.shutdown();
62
+ }
63
+ };
64
+
65
+ //#endregion
66
+ export { OtelProvider as default };
@@ -0,0 +1,90 @@
1
+ import { createRequire } from "node:module";
2
+ import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
3
+
4
+ //#region src/cli_instrumentation.ts
5
+ /**
6
+ * Default commands to exclude from tracing.
7
+ * These are typically scaffolding commands or long-running workers.
8
+ */
9
+ const DEFAULT_EXCLUDE = [
10
+ "make:*",
11
+ "generate:*",
12
+ "queue:work",
13
+ "queue:listen"
14
+ ];
15
+ /**
16
+ * Simple glob pattern matcher supporting only '*' wildcard
17
+ */
18
+ function matchPattern(commandName, pattern) {
19
+ if (pattern === "*") return true;
20
+ if (!pattern.includes("*")) return commandName === pattern;
21
+ return (/* @__PURE__ */ new RegExp(`^${pattern.replace(/\*/g, ".*")}$`)).test(commandName);
22
+ }
23
+ /**
24
+ * Check if a command should be traced based on include/exclude patterns
25
+ */
26
+ function shouldTraceCommand(commandName, config) {
27
+ if ((config.exclude ?? DEFAULT_EXCLUDE).some((pattern) => matchPattern(commandName, pattern))) return false;
28
+ if (config.include && config.include.length > 0) return config.include.some((pattern) => matchPattern(commandName, pattern));
29
+ return true;
30
+ }
31
+ /**
32
+ * Instrument AdonisJS Ace CLI commands with OpenTelemetry tracing.
33
+ *
34
+ * Uses monkey-patching of BaseCommand.prototype.exec to wrap command
35
+ * execution with an active OTEL span context. This ensures all child
36
+ * spans created during command execution are properly linked to the
37
+ * CLI command span.
38
+ *
39
+ * @param config - CLI tracing configuration
40
+ * @param appRoot - Application root directory path
41
+ */
42
+ async function instrumentCliCommands(config, appRoot) {
43
+ const tracer = trace.getTracer("@monocle.sh/adonisjs-agent", "1.0.0");
44
+ const BaseCommand = (await import(createRequire(appRoot ? `file://${appRoot}/package.json` : import.meta.url).resolve("@adonisjs/core/ace"))).BaseCommand;
45
+ const originalExec = BaseCommand.prototype.exec;
46
+ BaseCommand.prototype.exec = async function() {
47
+ const commandName = this.constructor.commandName;
48
+ const commandDescription = this.constructor.description;
49
+ if (!shouldTraceCommand(commandName, config)) return originalExec.call(this);
50
+ const span = tracer.startSpan(`cli ${commandName}`, {
51
+ kind: SpanKind.INTERNAL,
52
+ attributes: {
53
+ "entry_point.type": "cli",
54
+ "cli.command.name": commandName,
55
+ "cli.command.description": commandDescription || ""
56
+ }
57
+ });
58
+ const ctx = trace.setSpan(context.active(), span);
59
+ /**
60
+ * CRITICAL: Execute within context.with()
61
+ *
62
+ * This is what makes child spans work! Without this wrapper:
63
+ * - Child spans would have no parent
64
+ * - All spans would be root spans with different traceIds
65
+ * - The trace would be fragmented
66
+ *
67
+ * context.with() sets our span as the "active" span for all async
68
+ * operations within the callback, enabling proper trace propagation.
69
+ */
70
+ return context.with(ctx, async () => {
71
+ try {
72
+ return await originalExec.call(this);
73
+ } catch (error) {
74
+ if (error instanceof Error) {
75
+ span.recordException(error);
76
+ span.setStatus({
77
+ code: SpanStatusCode.ERROR,
78
+ message: error.message
79
+ });
80
+ }
81
+ throw error;
82
+ } finally {
83
+ span.end();
84
+ }
85
+ });
86
+ };
87
+ }
88
+
89
+ //#endregion
90
+ export { instrumentCliCommands };
@@ -0,0 +1,12 @@
1
+ import { MonocleConfig } from "./types.mjs";
2
+
3
+ //#region src/define_config.d.ts
4
+
5
+ /**
6
+ * Define and validate Monocle agent configuration.
7
+ * Sets up environment variables for OTEL exporters to point to Monocle.
8
+ * Returns undefined if no API key is provided (telemetry will be disabled).
9
+ */
10
+ declare function defineConfig(config: MonocleConfig): MonocleConfig | undefined;
11
+ //#endregion
12
+ export { defineConfig };
@@ -0,0 +1,72 @@
1
+ //#region src/define_config.ts
2
+ /**
3
+ * Extracts a header value from either ServerResponse (getHeader) or IncomingMessage (headers object).
4
+ */
5
+ function getHeader(response, name) {
6
+ if ("getHeader" in response && typeof response.getHeader === "function") {
7
+ const value = response.getHeader(name);
8
+ if (typeof value === "string") return value;
9
+ }
10
+ if ("headers" in response && response.headers) {
11
+ const value = response.headers[name];
12
+ if (typeof value === "string") return value;
13
+ }
14
+ }
15
+ /**
16
+ * Detects the connection type based on response headers.
17
+ * Returns 'sse' for Server-Sent Events, 'websocket' for WebSocket upgrades,
18
+ * or undefined for standard HTTP connections.
19
+ */
20
+ function detectConnectionType(response) {
21
+ if (getHeader(response, "content-type")?.includes("text/event-stream")) return "sse";
22
+ if (getHeader(response, "upgrade")?.toLowerCase() === "websocket") return "websocket";
23
+ }
24
+ /**
25
+ * Creates a response hook that detects long-running HTTP connections
26
+ * (SSE and WebSocket) and adds the `http.connection_type` attribute.
27
+ */
28
+ function createConnectionTypeHook(userHook) {
29
+ return (span, response) => {
30
+ const connectionType = detectConnectionType(response);
31
+ if (connectionType) span.setAttribute("http.connection_type", connectionType);
32
+ userHook?.(span, response);
33
+ };
34
+ }
35
+ /**
36
+ * Extracts the user-defined response hook from HTTP instrumentation config.
37
+ */
38
+ function extractUserResponseHook(httpConfig) {
39
+ if (typeof httpConfig !== "object" || httpConfig === null) return;
40
+ if (!("responseHook" in httpConfig)) return;
41
+ return httpConfig.responseHook;
42
+ }
43
+ /**
44
+ * Define and validate Monocle agent configuration.
45
+ * Sets up environment variables for OTEL exporters to point to Monocle.
46
+ * Returns undefined if no API key is provided (telemetry will be disabled).
47
+ */
48
+ function defineConfig(config) {
49
+ if (!config.apiKey) return;
50
+ const endpoint = config.endpoint || "https://ingest.monocle.sh";
51
+ const environment = config.environment || process.env.NODE_ENV || "development";
52
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT = endpoint;
53
+ process.env.OTEL_EXPORTER_OTLP_HEADERS = `x-api-key=${config.apiKey},x-monocle-env=${environment}`;
54
+ const httpConfig = config.instrumentations?.["@opentelemetry/instrumentation-http"];
55
+ const userResponseHook = extractUserResponseHook(httpConfig);
56
+ const instrumentations = {
57
+ ...config.instrumentations,
58
+ "@opentelemetry/instrumentation-http": {
59
+ ...typeof httpConfig === "object" && httpConfig !== null ? httpConfig : {},
60
+ responseHook: createConnectionTypeHook(userResponseHook)
61
+ }
62
+ };
63
+ return {
64
+ ...config,
65
+ endpoint,
66
+ environment,
67
+ instrumentations
68
+ };
69
+ }
70
+
71
+ //#endregion
72
+ export { defineConfig };
@@ -0,0 +1,27 @@
1
+ import { metrics } from "@opentelemetry/api";
2
+ import { HostMetrics } from "@opentelemetry/host-metrics";
3
+
4
+ //#region src/host_metrics.ts
5
+ let hostMetricsInstance = null;
6
+ /**
7
+ * Start collecting host metrics (CPU, Memory, Network, etc.)
8
+ *
9
+ * Uses @opentelemetry/host-metrics to collect:
10
+ * - system.cpu.time / system.cpu.utilization
11
+ * - system.memory.usage / system.memory.utilization
12
+ * - system.network.io / system.network.errors / system.network.dropped
13
+ * - process.cpu.time / process.cpu.utilization
14
+ * - process.memory.usage
15
+ */
16
+ function startHostMetrics(config = {}) {
17
+ if (hostMetricsInstance) return hostMetricsInstance;
18
+ hostMetricsInstance = new HostMetrics({
19
+ meterProvider: metrics.getMeterProvider(),
20
+ name: config.name || "monocle-host-metrics"
21
+ });
22
+ hostMetricsInstance.start();
23
+ return hostMetricsInstance;
24
+ }
25
+
26
+ //#endregion
27
+ export { startHostMetrics };
@@ -0,0 +1,28 @@
1
+ import { UserContextResult } from "@adonisjs/otel/types";
2
+
3
+ //#region src/monocle.d.ts
4
+ interface CaptureExceptionContext {
5
+ user?: {
6
+ id: string;
7
+ email?: string;
8
+ name?: string;
9
+ };
10
+ tags?: Record<string, string>;
11
+ extra?: Record<string, unknown>;
12
+ }
13
+ /**
14
+ * Monocle helper class for manual instrumentation.
15
+ */
16
+ declare class Monocle {
17
+ /**
18
+ * Capture an exception and record it on the current active span.
19
+ * If no span is active, the exception is silently ignored.
20
+ */
21
+ static captureException(error: unknown, context?: CaptureExceptionContext): void;
22
+ /**
23
+ * Set user information on the current active span.
24
+ */
25
+ static setUser(user: UserContextResult): void;
26
+ }
27
+ //#endregion
28
+ export { Monocle };
@@ -0,0 +1,39 @@
1
+ import { SpanStatusCode, trace } from "@opentelemetry/api";
2
+ import { setUser } from "@adonisjs/otel/helpers";
3
+
4
+ //#region src/monocle.ts
5
+ /**
6
+ * Monocle helper class for manual instrumentation.
7
+ */
8
+ var Monocle = class {
9
+ /**
10
+ * Capture an exception and record it on the current active span.
11
+ * If no span is active, the exception is silently ignored.
12
+ */
13
+ static captureException(error, context$1) {
14
+ const span = trace.getActiveSpan();
15
+ if (!span) return;
16
+ const err = error instanceof Error ? error : new Error(String(error));
17
+ span.recordException(err);
18
+ span.setStatus({
19
+ code: SpanStatusCode.ERROR,
20
+ message: err.message
21
+ });
22
+ if (context$1?.user) {
23
+ if (context$1.user.id) span.setAttribute("user.id", context$1.user.id);
24
+ if (context$1.user.email) span.setAttribute("user.email", context$1.user.email);
25
+ if (context$1.user.name) span.setAttribute("user.name", context$1.user.name);
26
+ }
27
+ if (context$1?.tags) for (const [key, value] of Object.entries(context$1.tags)) span.setAttribute(`monocle.tag.${key}`, value);
28
+ if (context$1?.extra) for (const [key, value] of Object.entries(context$1.extra)) span.setAttribute(`monocle.extra.${key}`, JSON.stringify(value));
29
+ }
30
+ /**
31
+ * Set user information on the current active span.
32
+ */
33
+ static setUser(user) {
34
+ setUser(user);
35
+ }
36
+ };
37
+
38
+ //#endregion
39
+ export { Monocle };
@@ -0,0 +1,114 @@
1
+ import { OtelConfig } from "@adonisjs/otel/types";
2
+
3
+ //#region src/types.d.ts
4
+
5
+ /**
6
+ * Configuration for CLI command tracing
7
+ */
8
+ interface CliTracingConfig {
9
+ /**
10
+ * Enable CLI command tracing.
11
+ * @default false
12
+ */
13
+ enabled?: boolean;
14
+ /**
15
+ * Commands to include in tracing. Supports glob patterns.
16
+ * If not specified, all commands are traced (except excluded ones).
17
+ * @example ['migration:*', 'db:*', 'my-custom:*']
18
+ */
19
+ include?: string[];
20
+ /**
21
+ * Commands to exclude from tracing. Supports glob patterns.
22
+ * @default ['make:*', 'generate:*', 'queue:work', 'queue:listen']
23
+ * @example ['make:*', 'generate:*']
24
+ */
25
+ exclude?: string[];
26
+ }
27
+ /**
28
+ * Configuration for host metrics collection
29
+ */
30
+ interface HostMetricsConfig {
31
+ /**
32
+ * Enable host metrics collection.
33
+ * @default true
34
+ */
35
+ enabled?: boolean;
36
+ /**
37
+ * Custom name for the meter.
38
+ * @default 'monocle-host-metrics'
39
+ */
40
+ name?: string;
41
+ }
42
+ /**
43
+ * Configuration for trace batching. Controls how spans are batched before export.
44
+ */
45
+ interface BatchConfig {
46
+ /**
47
+ * Maximum number of spans to include in a single batch.
48
+ * @default 512
49
+ */
50
+ maxExportBatchSize?: number;
51
+ /**
52
+ * Maximum time (ms) to wait before exporting a batch.
53
+ * @default 5000
54
+ */
55
+ scheduledDelayMillis?: number;
56
+ /**
57
+ * Maximum time (ms) to wait for a batch export to complete.
58
+ * @default 30000
59
+ */
60
+ exportTimeoutMillis?: number;
61
+ /**
62
+ * Maximum queue size. Spans will be dropped if the queue is full.
63
+ * @default 2048
64
+ */
65
+ maxQueueSize?: number;
66
+ }
67
+ interface MonocleConfig extends Omit<OtelConfig, 'traceExporter' | 'metricExporter'> {
68
+ /**
69
+ * Your Monocle API key. If not provided, telemetry will be disabled.
70
+ * Format: mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
71
+ */
72
+ apiKey?: string;
73
+ /**
74
+ * Ignore OPTIONS (CORS preflight) requests.
75
+ * These requests are typically not useful for tracing.
76
+ * @default true
77
+ */
78
+ ignoreOptionsRequests?: boolean;
79
+ /**
80
+ * Monocle ingestion endpoint.
81
+ * @default 'https://ingest.monocle.sh'
82
+ */
83
+ endpoint?: string;
84
+ /**
85
+ * Environment name sent with telemetry data.
86
+ * @default process.env.NODE_ENV || 'development'
87
+ */
88
+ environment?: string;
89
+ /**
90
+ * Host metrics configuration (CPU, Memory, Network, etc.).
91
+ * Set to `false` to disable, or pass config object.
92
+ * @default { enabled: true }
93
+ */
94
+ hostMetrics?: false | HostMetricsConfig;
95
+ /**
96
+ * Trace batching configuration. Batching reduces network overhead
97
+ * by grouping multiple spans into single requests.
98
+ */
99
+ batch?: BatchConfig;
100
+ /**
101
+ * Enable gzip compression for OTLP exports.
102
+ * Reduces bandwidth usage significantly.
103
+ * @default true
104
+ */
105
+ compression?: boolean;
106
+ /**
107
+ * CLI command tracing configuration.
108
+ * Set to `false` to disable, or pass config object.
109
+ * @default false
110
+ */
111
+ cli?: false | CliTracingConfig;
112
+ }
113
+ //#endregion
114
+ export { BatchConfig, CliTracingConfig, HostMetricsConfig, MonocleConfig };
@@ -0,0 +1,13 @@
1
+ {{{
2
+ exports({ to: app.configPath('monocle.ts') })
3
+ }}}
4
+ import { defineConfig } from '@monocle.sh/adonisjs-agent'
5
+ import env from '#start/env'
6
+
7
+ export default defineConfig({
8
+ apiKey: env.get('MONOCLE_API_KEY'),
9
+
10
+ serviceName: env.get('APP_NAME'),
11
+ serviceVersion: env.get('APP_VERSION'),
12
+ environment: env.get('APP_ENV'),
13
+ })
@@ -0,0 +1,8 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { dirname } from "node:path";
3
+
4
+ //#region stubs/main.ts
5
+ const stubsRoot = dirname(fileURLToPath(import.meta.url));
6
+
7
+ //#endregion
8
+ export { stubsRoot };
@@ -0,0 +1,4 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import { dirname } from 'node:path'
3
+
4
+ export const stubsRoot = dirname(fileURLToPath(import.meta.url))
@@ -0,0 +1,12 @@
1
+ {{{
2
+ exports({ to: app.makePath('otel.ts') })
3
+ }}}
4
+ /**
5
+ * Monocle agent initialization file.
6
+ *
7
+ * IMPORTANT: This file must be imported FIRST in bin/server.ts
8
+ * for auto-instrumentation to work correctly.
9
+ */
10
+ import { init } from '@monocle.sh/adonisjs-agent/init'
11
+
12
+ await init(import.meta.dirname)
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@monocle.sh/adonisjs-agent",
3
+ "version": "1.0.0-beta.4",
4
+ "description": "Monocle agent for AdonisJS - sends telemetry to Monocle cloud",
5
+ "keywords": [
6
+ "adonisjs",
7
+ "monocle",
8
+ "observability",
9
+ "opentelemetry",
10
+ "tracing"
11
+ ],
12
+ "license": "ISC",
13
+ "author": "Julien Ripouteau <julien@ripouteau.com>",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "type": "module",
18
+ "main": "./dist/index.mjs",
19
+ "module": "./dist/index.mjs",
20
+ "types": "./dist/index.d.mts",
21
+ "exports": {
22
+ ".": {
23
+ "dev": "./index.ts",
24
+ "default": "./dist/index.mjs"
25
+ },
26
+ "./init": {
27
+ "dev": "./src/init.ts",
28
+ "default": "./dist/init.mjs"
29
+ },
30
+ "./monocle_middleware": {
31
+ "dev": "./middleware/monocle_middleware.ts",
32
+ "default": "./dist/monocle_middleware.mjs"
33
+ },
34
+ "./monocle_provider": {
35
+ "dev": "./providers/monocle_provider.ts",
36
+ "default": "./dist/monocle_provider.mjs"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "exports": {
43
+ ".": "./dist/index.mjs",
44
+ "./init": "./dist/init.mjs",
45
+ "./monocle_middleware": "./dist/monocle_middleware.mjs",
46
+ "./monocle_provider": "./dist/monocle_provider.mjs",
47
+ "./package.json": "./package.json"
48
+ },
49
+ "tag": "beta"
50
+ },
51
+ "scripts": {
52
+ "build": "tsdown",
53
+ "dev": "tsdown",
54
+ "typecheck": "tsc --noEmit",
55
+ "release": "release-it",
56
+ "prepublishOnly": "pnpm run build"
57
+ },
58
+ "dependencies": {
59
+ "@adonisjs/otel": "1.1.0",
60
+ "@opentelemetry/api": "^1.9.0",
61
+ "@opentelemetry/core": "^2.2.0",
62
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.208.0",
63
+ "@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
64
+ "@opentelemetry/host-metrics": "^0.38.0",
65
+ "@opentelemetry/sdk-metrics": "^2.2.0",
66
+ "@opentelemetry/sdk-trace-base": "^2.2.0",
67
+ "@opentelemetry/semantic-conventions": "^1.38.0",
68
+ "import-in-the-middle": "^2.0.1"
69
+ },
70
+ "devDependencies": {
71
+ "@adonisjs/core": "catalog:",
72
+ "@adonisjs/tsconfig": "catalog:",
73
+ "release-it": "^19.2.2"
74
+ },
75
+ "peerDependencies": {
76
+ "@adonisjs/core": "^6.2.0 || ^7.0.0"
77
+ },
78
+ "packageManager": "pnpm@10.26.2"
79
+ }