@lensjs/adonis 1.0.12 → 1.2.0

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 CHANGED
@@ -1 +1,68 @@
1
- - first user should add the running console in bin/console file
1
+ # @lensjs/adonis
2
+
3
+ AdonisJS adapter for Lens. This package provides a service provider and middleware to seamlessly integrate the Lens monitoring and debugging tool into your AdonisJS applications. It enables automatic logging of requests, database queries, and cache events within the AdonisJS ecosystem.
4
+
5
+ ## Features
6
+
7
+ * **`LensServiceProvider`**: Registers the Lens configuration and initializes the Lens core with AdonisJS-specific adapters and watchers during the application boot process.
8
+ * **`AdonisAdapter` class**: Extends `LensAdapter` from `@lensjs/core`, providing AdonisJS-specific implementations for handling HTTP requests, database queries (via Adonis's `db:query` event), cache events, route registration, and serving the Lens UI.
9
+ * **`defineConfig` function**: A helper to define the Lens configuration within `config/lens.ts`, ensuring type safety and proper resolution.
10
+ * **`LensMiddleware`**: An AdonisJS middleware that generates a unique `requestId` for each incoming HTTP request, allowing all subsequent events (queries, cache) within that request's lifecycle to be correlated.
11
+ * **Request Watching**: Captures detailed information about HTTP requests, including method, URL, headers, body, status code, duration, and associated user (if configured).
12
+ * **Query Watching**: Hooks into AdonisJS's database query events to log SQL queries, their bindings, duration, and type.
13
+ * **Cache Watching**: Listens to AdonisJS cache events (read, write, hit, miss, delete, clear) to log cache interactions.
14
+ * **Exception Watching**: Captures and logs exceptions and errors within your AdonisJS application.
15
+ * **UI Serving**: Serves the Lens UI within your AdonisJS application at a configurable path.
16
+ * **Configurable Watchers**: Allows enabling or disabling specific watchers (requests, queries, cache) via the Lens configuration.
17
+ * **Authentication/User Context**: Supports optional `isAuthenticated` and `getUser` functions in the configuration to associate request logs with authenticated users.
18
+ * **Console/Command Ignoring**: Provides utilities to ignore logging for console commands or specific environments.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pnpm add @lensjs/adonis
24
+ node ace configure @lensjs/adonis
25
+ ```
26
+ (The `node ace configure` command will typically set up the service provider and config file.)
27
+
28
+ ## Usage Example (config/lens.ts)
29
+
30
+ ```typescript
31
+ import { defineConfig } from '@lensjs/adonis';
32
+
33
+ export default defineConfig({
34
+ appName: 'My Adonis App',
35
+ enabled: true, // Set to false in production
36
+ path: '/lens', // Access Lens UI at /lens
37
+ ignoredPaths: [], // Regex patterns for paths to ignore
38
+ onlyPaths: [], // Regex patterns for paths to only watch
39
+ watchers: {
40
+ queries: {
41
+ enabled: true,
42
+ provider: 'sqlite', // or 'postgresql', 'mysql', 'mariadb', 'plsql', 'transactsql'
43
+ },
44
+ cache: true,
45
+ requests: true,
46
+ exceptions: true,
47
+ },
48
+ // Optional: Integrate with your authentication system
49
+ isAuthenticated: async (ctx) => {
50
+ return !!ctx.auth.user;
51
+ },
52
+ getUser: async (ctx) => {
53
+ return { id: ctx.auth.user!.id, name: ctx.auth.user!.email };
54
+ },
55
+ });
56
+ ```
57
+
58
+ ## Usage Example (start/kernel.ts - Register Middleware)
59
+
60
+ ```typescript
61
+ // ... other imports
62
+ import LensMiddleware from '@lensjs/adonis/lens_middleware';
63
+
64
+ Server.middleware.global([
65
+ // ... other global middleware
66
+ () => new LensMiddleware(), // Register Lens middleware
67
+ ]);
68
+ ```
@@ -15,12 +15,14 @@ import { stubsRoot } from './stubs/main.js';
15
15
  const registerEnvValidation = async (codemods) => {
16
16
  try {
17
17
  await codemods.defineEnvValidations({
18
- leadingComment: 'Node lens variables',
18
+ leadingComment: 'LensJs variables',
19
19
  variables: {
20
20
  LENS_BASE_PATH: 'Env.schema.string.optional()',
21
21
  LENS_ENABLED: 'Env.schema.boolean.optional()',
22
22
  LENS_ENABLE_QUERY_WATCHER: 'Env.schema.boolean.optional()',
23
23
  LENS_ENABLE_REQUEST_WATCHER: 'Env.schema.boolean.optional()',
24
+ LENS_ENABLE_CACHE_WATCHER: 'Env.schema.boolean.optional()',
25
+ LENS_ENABLE_EXCEPTION_WATCHER: 'Env.schema.boolean.optional()',
24
26
  },
25
27
  });
26
28
  }
@@ -28,9 +30,22 @@ const registerEnvValidation = async (codemods) => {
28
30
  console.error(error);
29
31
  }
30
32
  };
33
+ const registerMiddleware = async (codemods) => {
34
+ try {
35
+ codemods.registerMiddleware('server', [
36
+ {
37
+ path: '@lensjs/adonis/lens_middleware',
38
+ },
39
+ ]);
40
+ }
41
+ catch (error) {
42
+ console.error(error);
43
+ }
44
+ };
31
45
  export async function configure(command) {
32
46
  const codemods = await command.createCodemods();
33
47
  await registerEnvValidation(codemods);
48
+ await registerMiddleware(codemods);
34
49
  await codemods.makeUsingStub(stubsRoot, 'config/lens.stub', {});
35
50
  await codemods.updateRcFile((rcFile) => {
36
51
  rcFile.addProvider('@lensjs/adonis/lens_provider');
@@ -1,10 +1,13 @@
1
+ import { Exception } from '@poppinss/utils';
1
2
  import type { ApplicationService } from '@adonisjs/core/types';
2
3
  import { LensConfig } from '../src/define_config.js';
3
4
  import { QueryEntry } from '@lensjs/core';
5
+ import { HttpContext } from '@adonisjs/core/http';
4
6
  declare module '@adonisjs/core/types' {
5
7
  interface ContainerBindings {
6
8
  lensConfig: LensConfig;
7
9
  queries?: QueryEntry['data'][];
10
+ watchExceptions?: (error: Exception, ctx: HttpContext) => Promise<void>;
8
11
  }
9
12
  }
10
13
  export default class LensServiceProvider {
@@ -1,6 +1,6 @@
1
1
  import { configProvider } from '@adonisjs/core';
2
2
  import { RuntimeException } from '@poppinss/utils';
3
- import { Lens, lensUtils, RequestWatcher, QueryWatcher } from '@lensjs/core';
3
+ import { Lens, lensUtils, RequestWatcher, QueryWatcher, CacheWatcher, lensExceptionUtils, ExceptionWatcher, handleUncaughExceptions, } from '@lensjs/core';
4
4
  import AdonisAdapter from '../src/adapter.js';
5
5
  export default class LensServiceProvider {
6
6
  app;
@@ -10,17 +10,30 @@ export default class LensServiceProvider {
10
10
  async boot() {
11
11
  const lensConfigProvider = this.app.config.get('lens');
12
12
  const config = await configProvider.resolve(this.app, lensConfigProvider);
13
+ const watchersMap = {
14
+ requests: new RequestWatcher(),
15
+ queries: new QueryWatcher(),
16
+ cache: new CacheWatcher(),
17
+ exceptions: new ExceptionWatcher(),
18
+ };
13
19
  if (!config) {
14
20
  throw new RuntimeException('Invalid "config/lens.ts" file. Make sure you are using the "defineConfig" method');
15
21
  }
16
22
  const { normalizedPath, ignoredPaths } = lensUtils.prepareIgnoredPaths(config.path, config.ignoredPaths);
17
23
  config.ignoredPaths = ignoredPaths;
24
+ if (config.watchers.exceptions) {
25
+ this.app.container.bindValue('watchExceptions', async (error, ctx) => {
26
+ const payload = lensExceptionUtils.constructErrorObject(error);
27
+ const requestId = ctx.request.lensEntry?.requestId;
28
+ await watchersMap.exceptions?.log({
29
+ ...payload,
30
+ requestId,
31
+ });
32
+ });
33
+ handleUncaughExceptions(watchersMap.exceptions);
34
+ }
18
35
  this.app.container.bindValue('lensConfig', config);
19
36
  this.app.booted(async () => {
20
- const watchersMap = {
21
- requests: new RequestWatcher(),
22
- queries: new QueryWatcher(),
23
- };
24
37
  const allowedWatchers = Object.keys(config.watchers)
25
38
  .filter((watcher) => config.watchers[watcher])
26
39
  .map((watcher) => watchersMap[watcher])
@@ -1,4 +1,4 @@
1
- import { RouteDefinition, LensAdapter, RequestWatcher, QueryWatcher } from '@lensjs/core';
1
+ import { RouteDefinition, LensAdapter, RequestWatcher, QueryWatcher, CacheWatcher } from '@lensjs/core';
2
2
  import type { ApplicationService, EmitterService, HttpRouterService } from '@adonisjs/core/types';
3
3
  import { LensConfig } from './define_config.js';
4
4
  export default class AdonisAdapter extends LensAdapter {
@@ -16,6 +16,7 @@ export default class AdonisAdapter extends LensAdapter {
16
16
  registerRoutes(routes: RouteDefinition[]): void;
17
17
  protected watchRequests(requestWatcher: RequestWatcher): void;
18
18
  protected watchQueries(queryWatcher: QueryWatcher): Promise<void>;
19
+ protected watchCache(watcher: CacheWatcher): Promise<void>;
19
20
  serveUI(uiPath: string, spaRoute: string, _dataToInject: Record<string, any>): void;
20
21
  private matchStaticFiles;
21
22
  private getUserFromContext;
@@ -1,8 +1,10 @@
1
1
  import { LensAdapter, WatcherTypeEnum, lensUtils, } from '@lensjs/core';
2
2
  import * as path from 'path';
3
- import { shouldIgnoreLogging } from './utils/index.js';
3
+ import { assertCacheBindingRegistered, shouldIgnoreLogging } from './utils/index.js';
4
4
  import string from '@adonisjs/core/helpers/string';
5
- import { nowISO, sqlDateTime } from '@lensjs/date';
5
+ import { HttpContext } from '@adonisjs/core/http';
6
+ import { nowISO } from '@lensjs/date';
7
+ import emitter from '@adonisjs/core/services/emitter';
6
8
  export default class AdonisAdapter extends LensAdapter {
7
9
  app;
8
10
  router;
@@ -28,6 +30,9 @@ export default class AdonisAdapter extends LensAdapter {
28
30
  this.queryWatcher = watcher;
29
31
  await this.watchQueries(watcher);
30
32
  break;
33
+ case WatcherTypeEnum.CACHE:
34
+ this.watchCache(watcher);
35
+ break;
31
36
  }
32
37
  }
33
38
  });
@@ -54,7 +59,7 @@ export default class AdonisAdapter extends LensAdapter {
54
59
  if (self.shouldIgnorePath(event.ctx.request.url(false)))
55
60
  return;
56
61
  const request = event.ctx.request;
57
- const requestId = lensUtils.generateRandomUuid();
62
+ const requestId = event.ctx.request.lensEntry?.requestId ?? lensUtils.generateRandomUuid();
58
63
  const logPayload = {
59
64
  request: {
60
65
  id: requestId,
@@ -82,20 +87,89 @@ export default class AdonisAdapter extends LensAdapter {
82
87
  if (shouldIgnoreLogging(self.app))
83
88
  return;
84
89
  // @ts-ignore
85
- this.emitter.on('db:query', async function (query) {
90
+ emitter.on('db:query', async function (query) {
91
+ const requestId = HttpContext.get()?.request.lensEntry?.requestId;
86
92
  const duration = query.duration ? string.prettyHrTime(query.duration) : '0 ms';
87
93
  const payload = {
88
94
  query: lensUtils.formatSqlQuery(lensUtils.interpolateQuery(query.sql, query.bindings), self.config.watchers.queries.provider),
89
95
  duration,
90
- createdAt: sqlDateTime(),
96
+ createdAt: nowISO(),
91
97
  type: self.config.watchers.queries.provider,
92
98
  };
93
99
  await queryWatcher.log({
94
100
  data: payload,
101
+ requestId,
95
102
  });
96
103
  });
97
104
  });
98
105
  }
106
+ async watchCache(watcher) {
107
+ if (!this.config.watchers.cache)
108
+ return;
109
+ assertCacheBindingRegistered(this.app);
110
+ // Clear
111
+ // @ts-expect-error
112
+ this.emitter.on('cache:cleared', async (event) => {
113
+ await watcher.log({
114
+ action: 'clear',
115
+ data: {
116
+ key: event.key,
117
+ },
118
+ createdAt: nowISO(),
119
+ requestId: HttpContext.get()?.request.lensEntry?.requestId,
120
+ });
121
+ });
122
+ // Write
123
+ // @ts-expect-error
124
+ this.emitter.on('cache:written', async (event) => {
125
+ await watcher.log({
126
+ action: 'write',
127
+ data: {
128
+ key: event.key,
129
+ value: event.value,
130
+ },
131
+ createdAt: nowISO(),
132
+ requestId: HttpContext.get()?.request.lensEntry?.requestId,
133
+ });
134
+ });
135
+ // Hit
136
+ // @ts-expect-error
137
+ this.emitter.on('cache:hit', async (event) => {
138
+ await watcher.log({
139
+ action: 'hit',
140
+ data: {
141
+ key: event.key,
142
+ value: event.value,
143
+ },
144
+ createdAt: nowISO(),
145
+ requestId: HttpContext.get()?.request.lensEntry?.requestId,
146
+ });
147
+ });
148
+ // Miss
149
+ // @ts-expect-error
150
+ this.emitter.on('cache:miss', async (event) => {
151
+ await watcher.log({
152
+ action: 'miss',
153
+ data: {
154
+ key: event.key,
155
+ },
156
+ createdAt: nowISO(),
157
+ requestId: HttpContext.get()?.request.lensEntry?.requestId,
158
+ });
159
+ });
160
+ // Delete
161
+ // @ts-expect-error
162
+ this.emitter.on('cache:deleted', async (event) => {
163
+ await watcher.log({
164
+ action: 'delete',
165
+ data: {
166
+ key: event.key,
167
+ },
168
+ createdAt: nowISO(),
169
+ requestId: HttpContext.get()?.request.lensEntry?.requestId,
170
+ });
171
+ });
172
+ }
99
173
  serveUI(uiPath, spaRoute, _dataToInject) {
100
174
  this.app.booted(async () => {
101
175
  this.router.get(`/${spaRoute}/favicon.ico`, (ctx) => {
@@ -12,7 +12,9 @@ export type LensConfig = {
12
12
  enabled: boolean;
13
13
  provider: AdonisQueryType;
14
14
  };
15
+ cache: boolean;
15
16
  requests: boolean;
17
+ exceptions: boolean;
16
18
  };
17
19
  isAuthenticated?: (ctx: HttpContext) => Promise<boolean>;
18
20
  getUser?: (ctx: HttpContext) => Promise<UserEntry>;
@@ -0,0 +1,12 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { NextFn } from '@adonisjs/core/types/http';
3
+ declare module '@adonisjs/core/http' {
4
+ interface Request {
5
+ lensEntry?: {
6
+ requestId: string;
7
+ };
8
+ }
9
+ }
10
+ export default class LensMiddleware {
11
+ handle(ctx: HttpContext, next: NextFn): Promise<any>;
12
+ }
@@ -0,0 +1,10 @@
1
+ import { lensUtils } from '@lensjs/core';
2
+ export default class LensMiddleware {
3
+ async handle(ctx, next) {
4
+ const lensEntry = {
5
+ requestId: lensUtils.generateRandomUuid(),
6
+ };
7
+ ctx.request.lensEntry = lensEntry;
8
+ return await next();
9
+ }
10
+ }
@@ -3,6 +3,7 @@ import { HttpContext } from '@adonisjs/core/http';
3
3
  import { LensConfig } from '../define_config.js';
4
4
  export declare const runningInConsole: (app: ApplicationService) => boolean;
5
5
  export declare const getRunningCommand: () => string;
6
+ export declare const assertCacheBindingRegistered: (app: ApplicationService) => void;
6
7
  export declare function allowedCommands(): boolean;
7
8
  export declare const shouldIgnoreLogging: (app: ApplicationService) => boolean;
8
9
  export declare const resolveConfigFromContext: (ctx: HttpContext) => Promise<LensConfig>;
@@ -20,6 +20,16 @@ before creating a new Ignitor instance in:
20
20
  }
21
21
  return cmd;
22
22
  };
23
+ export const assertCacheBindingRegistered = (app) => {
24
+ if (!app.container.hasBinding('cache.manager')) {
25
+ const message = `
26
+ ${chalk.red.bold('✖ Cache binding not registered')}
27
+
28
+ Make sure to install @adonisjs/cache package to use cache watcher.
29
+ `;
30
+ throw new Error(message);
31
+ }
32
+ };
23
33
  export function allowedCommands() {
24
34
  const command = getRunningCommand();
25
35
  return ['queue:listen', 'schedule:work'].includes(command ?? 'WRONG');
@@ -12,6 +12,8 @@ const lensConfig = defineConfig({
12
12
  onlyPaths: [],
13
13
  watchers: {
14
14
  requests: env.get('LENS_ENABLE_REQUEST_WATCHER', true),
15
+ cache: env.get('LENS_ENABLE_CACHE_WATCHER', false),
16
+ exceptions: env.get('LENS_ENABLE_EXCEPTION_WATCHER', true),
15
17
  queries: {
16
18
  enabled: env.get('LENS_ENABLE_QUERY_WATCHER', true),
17
19
  provider: 'sqlite', // Change to your database provider
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lensjs/adonis",
3
3
  "description": "AdonisJs adapter for lens package",
4
- "version": "1.0.12",
4
+ "version": "1.2.0",
5
5
  "engines": {
6
6
  "node": ">=20.6.0"
7
7
  },
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@poppinss/utils": "^6.10.1",
49
49
  "chalk": "^5.6.0",
50
- "@lensjs/core": "1.0.12",
50
+ "@lensjs/core": "2.1.0",
51
51
  "@lensjs/date": "1.0.12"
52
52
  },
53
53
  "publishConfig": {
@@ -78,7 +78,6 @@
78
78
  "lint": "eslint .",
79
79
  "format": "prettier --write .",
80
80
  "quick:test": "node --import=./tsnode.esm.js --enable-source-maps bin/test.ts",
81
- "pretest": "npm run lint",
82
81
  "test": "c8 npm run quick:test",
83
82
  "prebuild": "npm run clean",
84
83
  "build": "tsc",