@aurostack/stacks 0.1.1 → 0.2.1

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.
Files changed (112) hide show
  1. package/README.md +59 -0
  2. package/cli/src/generate.mjs +13 -1
  3. package/cli/stack.mjs +1 -1
  4. package/package.json +1 -1
  5. package/templates/nest-api/files/.bin/secrets.ts +15 -6
  6. package/templates/nest-api/files/.env.example +29 -2
  7. package/templates/nest-api/files/.env.test.example +1 -1
  8. package/templates/nest-api/files/_gitignore +1 -0
  9. package/templates/nest-api/files/compose.yml +16 -0
  10. package/templates/nest-api/files/package.json +18 -3
  11. package/templates/nest-api/files/src/app.module.ts +9 -1
  12. package/templates/nest-api/files/src/app.setup.ts +8 -7
  13. package/templates/nest-api/files/src/common/__tests__/temporal.service.spec.ts +66 -0
  14. package/templates/nest-api/files/src/common/common.module.ts +4 -3
  15. package/templates/nest-api/files/src/common/constants/pino.ts +14 -0
  16. package/templates/nest-api/files/src/common/controllers/index.ts +0 -1
  17. package/templates/nest-api/files/src/common/modules/index.ts +0 -1
  18. package/templates/nest-api/files/src/common/modules/logger.module.ts +5 -8
  19. package/templates/nest-api/files/src/common/services/config.service.ts +43 -5
  20. package/templates/nest-api/files/src/common/services/index.ts +1 -0
  21. package/templates/nest-api/files/src/common/services/temporal.service.ts +66 -0
  22. package/templates/nest-api/files/src/instrumentation.ts +117 -0
  23. package/templates/nest-api/files/src/main.ts +3 -1
  24. package/templates/nest-api/files/src/notifications/services/notifications.service.ts +4 -2
  25. package/templates/nest-api/files/src/telemetry/__tests__/config.spec.ts +67 -0
  26. package/templates/nest-api/files/src/telemetry/config.ts +62 -0
  27. package/templates/nest-api/template.json +54 -14
  28. package/templates/node-worker/files/.bin/secrets.ts +15 -6
  29. package/templates/node-worker/files/.env.example +27 -0
  30. package/templates/node-worker/files/Dockerfile +25 -2
  31. package/templates/node-worker/files/Dockerfile.dev +25 -2
  32. package/templates/node-worker/files/_gitignore +1 -0
  33. package/templates/node-worker/files/ecosystem.config.cjs +2 -0
  34. package/templates/node-worker/files/package.json +24 -4
  35. package/templates/node-worker/files/src/config.ts +23 -0
  36. package/templates/node-worker/files/src/index.ts +40 -4
  37. package/templates/node-worker/files/src/instrumentation.ts +110 -0
  38. package/templates/node-worker/files/src/logger.ts +14 -1
  39. package/templates/node-worker/files/src/telemetry/config.ts +61 -0
  40. package/templates/node-worker/files/src/temporal/activities/index.ts +14 -0
  41. package/templates/node-worker/files/src/temporal/worker.ts +84 -0
  42. package/templates/node-worker/files/src/temporal/workflows/index.ts +23 -0
  43. package/templates/node-worker/files/src/utils/worker.ts +10 -2
  44. package/templates/node-worker/template.json +43 -1
  45. package/templates/py-worker/files/.env.example +28 -0
  46. package/templates/py-worker/files/config.py +32 -1
  47. package/templates/py-worker/files/requirements.txt +16 -0
  48. package/templates/py-worker/files/temporal/__init__.py +0 -0
  49. package/templates/py-worker/files/temporal/activities.py +13 -0
  50. package/templates/py-worker/files/temporal/workflows.py +38 -0
  51. package/templates/py-worker/files/utils/db.py +20 -27
  52. package/templates/py-worker/files/utils/logger.py +9 -2
  53. package/templates/py-worker/files/utils/telemetry.py +154 -0
  54. package/templates/py-worker/files/utils/temporal.py +55 -0
  55. package/templates/py-worker/files/utils/worker.py +14 -3
  56. package/templates/py-worker/files/worker.py +16 -0
  57. package/templates/py-worker/template.json +36 -0
  58. package/templates/react-app/derive.sh +2 -1
  59. package/templates/react-app/files/.bin/secrets.js +8 -2
  60. package/templates/react-app/files/.env.example +11 -0
  61. package/templates/react-app/files/.forgejo/workflows/build-and-push.yml +3 -0
  62. package/templates/react-app/files/Dockerfile +10 -0
  63. package/templates/react-app/files/_gitignore +1 -0
  64. package/templates/react-app/files/build.sh +3 -1
  65. package/templates/react-app/files/package.json +2 -0
  66. package/templates/react-app/files/src/lib/env.ts +10 -0
  67. package/templates/react-app/files/src/main.tsx +4 -0
  68. package/templates/react-app/files/src/shared/auth/provider.tsx +2 -0
  69. package/templates/react-app/files/src/shared/layouts/error-boundary.tsx +2 -0
  70. package/templates/react-app/files/src/shared/telemetry/index.ts +80 -0
  71. package/templates/react-app/template.json +16 -2
  72. package/templates/react-monorepo/files/.forgejo/workflows/build-and-push.yml +15 -11
  73. package/templates/react-monorepo/files/Dockerfile +21 -7
  74. package/templates/react-monorepo/files/_gitignore +1 -0
  75. package/templates/react-monorepo/files/apps/admin/.bin/secrets.js +8 -3
  76. package/templates/react-monorepo/files/apps/admin/.env.example +11 -0
  77. package/templates/react-monorepo/files/apps/admin/package.json +1 -0
  78. package/templates/react-monorepo/files/apps/admin/src/app/router.tsx +11 -7
  79. package/templates/react-monorepo/files/apps/admin/src/lib/env.ts +10 -0
  80. package/templates/react-monorepo/files/apps/admin/src/main.tsx +4 -0
  81. package/templates/react-monorepo/files/apps/auth/.bin/secrets.js +8 -3
  82. package/templates/react-monorepo/files/apps/auth/.env.example +11 -0
  83. package/templates/react-monorepo/files/apps/auth/package.json +1 -0
  84. package/templates/react-monorepo/files/apps/auth/src/lib/env.ts +10 -0
  85. package/templates/react-monorepo/files/apps/auth/src/main.tsx +4 -0
  86. package/templates/react-monorepo/files/apps/client/.bin/secrets.js +8 -3
  87. package/templates/react-monorepo/files/apps/client/.env.example +11 -0
  88. package/templates/react-monorepo/files/apps/client/package.json +1 -0
  89. package/templates/react-monorepo/files/apps/client/src/lib/env.ts +10 -0
  90. package/templates/react-monorepo/files/apps/client/src/main.tsx +4 -0
  91. package/templates/react-monorepo/files/apps/client/src/vite-env.d.ts +1 -1
  92. package/templates/react-monorepo/files/apps/landing/.bin/secrets.js +8 -3
  93. package/templates/react-monorepo/files/apps/landing/.env.example +11 -0
  94. package/templates/react-monorepo/files/apps/landing/package.json +1 -0
  95. package/templates/react-monorepo/files/apps/landing/src/lib/env.ts +10 -0
  96. package/templates/react-monorepo/files/apps/landing/src/main.tsx +4 -0
  97. package/templates/react-monorepo/files/build.sh +3 -1
  98. package/templates/react-monorepo/files/packages/auth/package.json +1 -0
  99. package/templates/react-monorepo/files/packages/auth/src/provider.tsx +2 -0
  100. package/templates/react-monorepo/files/packages/layouts/package.json +1 -0
  101. package/templates/react-monorepo/files/packages/layouts/src/error-boundary.tsx +2 -0
  102. package/templates/react-monorepo/files/packages/telemetry/eslint.config.mjs +3 -0
  103. package/templates/react-monorepo/files/packages/telemetry/package.json +20 -0
  104. package/templates/react-monorepo/files/packages/telemetry/src/index.ts +80 -0
  105. package/templates/react-monorepo/files/packages/telemetry/tsconfig.json +4 -0
  106. package/templates/react-monorepo/files/packages/types/.bin/secrets.js +8 -3
  107. package/templates/react-monorepo/files/turbo.json +1 -1
  108. package/templates/react-monorepo/template.json +87 -9
  109. package/templates/nest-api/files/src/common/controllers/metrics.controller.ts +0 -21
  110. package/templates/nest-api/files/src/common/interceptors/index.ts +0 -1
  111. package/templates/nest-api/files/src/common/interceptors/metrics.interceptor.ts +0 -37
  112. package/templates/nest-api/files/src/common/modules/metrics.module.ts +0 -28
@@ -1,10 +1,14 @@
1
1
  import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
+ import { initTelemetry } from '@acme/telemetry'; // @feature telemetry
4
+ import { API_URL, TELEMETRY } from './lib/env'; // @feature telemetry
3
5
  import { RouterProvider } from 'react-router';
4
6
  import '@acme/ui/globals.css';
5
7
  import { Providers } from './app/providers';
6
8
  import { router } from './app/router';
7
9
 
10
+ initTelemetry({ service: 'acme-landing', ...TELEMETRY, apiUrls: [API_URL] }); // @feature telemetry
11
+
8
12
  createRoot(document.getElementById('root')!).render(
9
13
  <StrictMode>
10
14
  <Providers>
@@ -46,7 +46,9 @@ read_var() {
46
46
  }
47
47
 
48
48
  BUILD_ARGS="--build-arg APP=$APP"
49
- for VAR in VITE_APP_API_URL VITE_APP_AUTH_HOST VITE_APP_CLIENT_HOST VITE_APP_ADMIN_HOST VITE_APP_LANDING VITE_APP_SUPPORT_EMAIL; do
49
+ VARS="VITE_APP_API_URL VITE_APP_AUTH_HOST VITE_APP_CLIENT_HOST VITE_APP_ADMIN_HOST VITE_APP_LANDING VITE_APP_SUPPORT_EMAIL VITE_APP_SITE_URL"
50
+ VARS="$VARS VITE_OPENOBSERVE_URL VITE_OPENOBSERVE_ORG VITE_OPENOBSERVE_CLIENT_TOKEN" # @feature telemetry
51
+ for VAR in $VARS; do
50
52
  VALUE="$(read_var "$VAR")"
51
53
  [ -n "$VALUE" ] && BUILD_ARGS="$BUILD_ARGS --build-arg $VAR=$VALUE"
52
54
  done
@@ -11,6 +11,7 @@
11
11
  "typecheck": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
+ "@acme/telemetry": "workspace:*",
14
15
  "better-auth": "^1.6.2"
15
16
  },
16
17
  "peerDependencies": {
@@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux';
3
3
  import { getAuthClient } from './client';
4
4
  import { sessionCleared, sessionLoaded, sessionLoading, type AuthUser } from './slice';
5
5
  import { AUTH_BROADCAST_CHANNEL } from './logout';
6
+ import { setTelemetryUser } from '@acme/telemetry'; // @feature telemetry
6
7
 
7
8
  function toAuthUser(user: Record<string, unknown> | null | undefined): AuthUser | null {
8
9
  if (!user) return null;
@@ -33,6 +34,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
33
34
  }
34
35
  const user = (session.data?.user as Record<string, unknown> | undefined) ?? null;
35
36
  dispatch(sessionLoaded(toAuthUser(user)));
37
+ setTelemetryUser(user ? { id: String(user.id) } : null); // @feature telemetry
36
38
  }, [session.isPending, session.data, dispatch]);
37
39
 
38
40
  useEffect(() => {
@@ -11,6 +11,7 @@
11
11
  "typecheck": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
+ "@acme/telemetry": "workspace:*",
14
15
  "@acme/ui": "workspace:*",
15
16
  "lucide-react": "^1.17.0"
16
17
  },
@@ -1,6 +1,7 @@
1
1
  import { Component, type ErrorInfo, type ReactNode } from 'react';
2
2
  import { Button } from '@acme/ui';
3
3
  import { StateScreen } from './states/state-screen';
4
+ import { reportError } from '@acme/telemetry'; // @feature telemetry
4
5
 
5
6
  type ErrorBoundaryProps = {
6
7
  children: ReactNode;
@@ -21,6 +22,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
21
22
 
22
23
  componentDidCatch(error: Error, info: ErrorInfo): void {
23
24
  console.error('ErrorBoundary caught an error', error, info);
25
+ reportError(error, { componentStack: info.componentStack }); // @feature telemetry
24
26
  }
25
27
 
26
28
  render(): ReactNode {
@@ -0,0 +1,3 @@
1
+ import config from '@acme/config/eslint';
2
+
3
+ export default config;
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@acme/telemetry",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "lint": "eslint .",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@openobserve/browser-logs": "^0.4.3",
15
+ "@openobserve/browser-rum": "^0.4.3"
16
+ },
17
+ "devDependencies": {
18
+ "@acme/config": "workspace:*"
19
+ }
20
+ }
@@ -0,0 +1,80 @@
1
+ // @acme/telemetry — browser monitoring (RUM) and logs, sent to OpenObserve.
2
+ //
3
+ // Page views, errors, slow resources and user actions, plus console errors as
4
+ // logs. Requests to the app's own API carry a W3C `traceparent` header, so a
5
+ // click links to the backend trace it caused. The SDK finishes starting a
6
+ // moment after init (its session store is async), so the first request or two
7
+ // of a page load (typically the session check) go out without the header.
8
+ // Off until connection settings are passed; everything here is then a no-op.
9
+ import { DefaultPrivacyLevel, openobserveRum } from '@openobserve/browser-rum';
10
+ import { openobserveLogs } from '@openobserve/browser-logs';
11
+
12
+ export interface TelemetryOptions {
13
+ /** Service name, e.g. `acme-client` — one per app. */
14
+ service: string;
15
+ /** OpenObserve base URL, e.g. `https://o2.example.com`. */
16
+ url?: string;
17
+ /** The project's organization identifier. */
18
+ org?: string;
19
+ /**
20
+ * A RUM client token (Ingestion → RUM). Write-only and built for shipping
21
+ * in a bundle — never the backend's ingestion token.
22
+ */
23
+ clientToken?: string;
24
+ /** `development`, `production`, … — Vite's `import.meta.env.MODE`. */
25
+ environment?: string;
26
+ version?: string;
27
+ /** Backend origin(s) whose requests should carry trace headers. */
28
+ apiUrls?: string[];
29
+ /** Share of sessions to record for replay, 0–100. Off by default. */
30
+ sessionReplaySampleRate?: number;
31
+ }
32
+
33
+ let started = false;
34
+
35
+ export function initTelemetry(options: TelemetryOptions): boolean {
36
+ const { url, org, clientToken } = options;
37
+ if (started || !url || !org || !clientToken) return false;
38
+
39
+ // The SDK takes a bare host (no scheme) plus an explicit plain-HTTP flag.
40
+ const { host, protocol } = new URL(url);
41
+ const common = {
42
+ clientToken,
43
+ site: host,
44
+ organizationIdentifier: org,
45
+ insecureHTTP: protocol === 'http:',
46
+ service: options.service,
47
+ env: options.environment,
48
+ version: options.version
49
+ };
50
+
51
+ openobserveRum.init({
52
+ ...common,
53
+ applicationId: options.service,
54
+ sessionSampleRate: 100,
55
+ sessionReplaySampleRate: options.sessionReplaySampleRate ?? 0,
56
+ defaultPrivacyLevel: DefaultPrivacyLevel.MASK_USER_INPUT,
57
+ trackResources: true,
58
+ trackLongTasks: true,
59
+ trackUserInteractions: true,
60
+ allowedTracingUrls: (options.apiUrls ?? []).filter(Boolean).map((match) => ({
61
+ match,
62
+ propagatorTypes: ['tracecontext' as const]
63
+ }))
64
+ });
65
+ openobserveLogs.init({ ...common, forwardErrorsToLogs: true });
66
+ started = true;
67
+ return true;
68
+ }
69
+
70
+ /** Report a handled error — e.g. from an error boundary. */
71
+ export function reportError(error: unknown, context?: object): void {
72
+ if (started) openobserveRum.addError(error, context);
73
+ }
74
+
75
+ /** Tie the session to a signed-in user (id only), or clear it on logout. */
76
+ export function setTelemetryUser(user: { id: string } | null): void {
77
+ if (!started) return;
78
+ if (user) openobserveRum.setUser({ id: user.id });
79
+ else openobserveRum.clearUser();
80
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "@acme/config/tsconfig/react.json",
3
+ "include": ["src"]
4
+ }
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
52
52
  const envFile = ENV_FILES[env];
53
53
 
54
54
  console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
55
- execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env} > ${envFile}`, {
56
- stdio: 'inherit'
55
+ // Export first and write only on success: a shell `>` redirect would empty
56
+ // the existing file before infisical had the chance to fail. execSync throws
57
+ // (and the process exits non-zero) when infisical fails.
58
+ const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
59
+ stdio: ['inherit', 'pipe', 'inherit']
57
60
  });
61
+ writeFileSync(envFile, output);
62
+ console.log(`Wrote ${envFile}`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://turbo.build/schema.json",
3
3
  "ui": "tui",
4
- "globalDependencies": [".env", "tsconfig.base.json"],
4
+ "globalDependencies": [".env", "packages/config/tsconfig/base.json"],
5
5
  "tasks": {
6
6
  "build": {
7
7
  "dependsOn": ["^build"],
@@ -81,6 +81,52 @@
81
81
  "packages/types/**"
82
82
  ]
83
83
  },
84
+ "telemetry": {
85
+ "title": "Browser monitoring → OpenObserve",
86
+ "description": "The @acme/telemetry package: OpenObserve RUM and logs in every app — page views, errors (including the error boundary's), slow resources, user actions, the signed-in user id, and traceparent headers on API calls so frontend and backend traces link up. Session replay is off unless a project turns it on. Off at runtime until VITE_OPENOBSERVE_* are set.",
87
+ "default": true,
88
+ "files": [
89
+ "packages/telemetry/**"
90
+ ],
91
+ "packageJson": [
92
+ {
93
+ "file": "apps/admin/package.json",
94
+ "dependencies": [
95
+ "@acme/telemetry"
96
+ ]
97
+ },
98
+ {
99
+ "file": "apps/auth/package.json",
100
+ "dependencies": [
101
+ "@acme/telemetry"
102
+ ]
103
+ },
104
+ {
105
+ "file": "apps/client/package.json",
106
+ "dependencies": [
107
+ "@acme/telemetry"
108
+ ]
109
+ },
110
+ {
111
+ "file": "apps/landing/package.json",
112
+ "dependencies": [
113
+ "@acme/telemetry"
114
+ ]
115
+ },
116
+ {
117
+ "file": "packages/layouts/package.json",
118
+ "dependencies": [
119
+ "@acme/telemetry"
120
+ ]
121
+ },
122
+ {
123
+ "file": "packages/auth/package.json",
124
+ "dependencies": [
125
+ "@acme/telemetry"
126
+ ]
127
+ }
128
+ ]
129
+ },
84
130
  "app-auth": {
85
131
  "title": "Auth app",
86
132
  "description": "Login, signup, forgot/reset password, email verification. The one app that owns the session; the others redirect here.",
@@ -99,14 +145,14 @@
99
145
  },
100
146
  "app-admin": {
101
147
  "title": "Admin console",
102
- "description": "Role-gated internal console over the same data layer.",
148
+ "description": "Internal console over the same data layer, gated to the admin and superuser roles in the UI. The API must enforce the role on every endpoint it calls.",
103
149
  "files": [
104
150
  "apps/admin/**"
105
151
  ]
106
152
  },
107
153
  "app-landing": {
108
154
  "title": "Marketing site",
109
- "description": "Public landing site with a prerender step for static routes.",
155
+ "description": "Public marketing site: a home page, an SEO route table, and build-time robots.txt, sitemap.xml and security.txt.",
110
156
  "files": [
111
157
  "apps/landing/**"
112
158
  ]
@@ -127,7 +173,7 @@
127
173
  },
128
174
  "pwa": {
129
175
  "title": "Installable PWA (client app)",
130
- "description": "vite-plugin-pwa: app-shell offline caching, auto-update with an update prompt, and a custom install button.",
176
+ "description": "vite-plugin-pwa configured for the client app: app-shell offline caching, and registerType 'prompt' so a new build waits for the user instead of swapping assets under a running app. The update prompt and install button are yours to build with the plugin's useRegisterSW.",
131
177
  "requires": [
132
178
  "app-client"
133
179
  ],
@@ -140,7 +186,7 @@
140
186
  },
141
187
  "charts": {
142
188
  "title": "Charts (client app)",
143
- "description": "ApexCharts via react-apexcharts, theme-aware through the shared chart palette.",
189
+ "description": "ApexCharts and react-apexcharts in the client app, with a theme-aware chart palette (the --chart-* tokens in the UI kit). No chart components ship.",
144
190
  "requires": [
145
191
  "app-client"
146
192
  ],
@@ -193,11 +239,43 @@
193
239
  "apps/*/.bin/**",
194
240
  "packages/types/.bin/**"
195
241
  ],
196
- "packageJson": {
197
- "scripts": [
198
- "secrets"
199
- ]
200
- }
242
+ "packageJson": [
243
+ {
244
+ "scripts": [
245
+ "secrets"
246
+ ]
247
+ },
248
+ {
249
+ "file": "apps/admin/package.json",
250
+ "scripts": [
251
+ "secrets"
252
+ ]
253
+ },
254
+ {
255
+ "file": "apps/auth/package.json",
256
+ "scripts": [
257
+ "secrets"
258
+ ]
259
+ },
260
+ {
261
+ "file": "apps/client/package.json",
262
+ "scripts": [
263
+ "secrets"
264
+ ]
265
+ },
266
+ {
267
+ "file": "apps/landing/package.json",
268
+ "scripts": [
269
+ "secrets"
270
+ ]
271
+ },
272
+ {
273
+ "file": "packages/types/package.json",
274
+ "scripts": [
275
+ "secrets"
276
+ ]
277
+ }
278
+ ]
201
279
  }
202
280
  },
203
281
  "hooks": [
@@ -1,21 +0,0 @@
1
- import { Controller, Get, Res } from '@nestjs/common';
2
- import { ApiExcludeEndpoint, ApiTags } from '@nestjs/swagger';
3
- import { SkipThrottle } from '@nestjs/throttler'; // @feature rate-limit
4
- import { AllowAnonymous } from '@thallesp/nestjs-better-auth';
5
- import { PrometheusController } from '@willsoto/nestjs-prometheus';
6
- import type { Response } from 'express';
7
-
8
- @ApiTags('App')
9
- @Controller()
10
- // @feature:start rate-limit
11
- // Scraped on a fixed interval by Prometheus; already gated by basic auth.
12
- @SkipThrottle()
13
- // @feature:end
14
- export class MetricsController extends PrometheusController {
15
- @Get()
16
- @ApiExcludeEndpoint()
17
- @AllowAnonymous()
18
- async index(@Res({ passthrough: true }) response: Response): Promise<string> {
19
- return super.index(response);
20
- }
21
- }
@@ -1 +0,0 @@
1
- export * from './metrics.interceptor'; // @feature observability
@@ -1,37 +0,0 @@
1
- import {
2
- Injectable,
3
- NestInterceptor,
4
- ExecutionContext,
5
- CallHandler
6
- } from '@nestjs/common';
7
- import { InjectMetric } from '@willsoto/nestjs-prometheus';
8
- import { Histogram } from 'prom-client';
9
- import { Observable, tap } from 'rxjs';
10
-
11
- @Injectable()
12
- export class MetricsInterceptor implements NestInterceptor {
13
- constructor(
14
- @InjectMetric('http_request_duration_seconds')
15
- private readonly histogram: Histogram<string>
16
- ) {}
17
-
18
- intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
19
- if (context.getType() !== 'http') {
20
- return next.handle();
21
- }
22
-
23
- const http = context.switchToHttp();
24
- const req = http.getRequest();
25
- const res = http.getResponse();
26
- const stop = this.histogram.startTimer();
27
-
28
- const observe = () =>
29
- stop({
30
- method: req.method,
31
- route: req.route?.path ?? 'unmatched',
32
- status: res.statusCode
33
- });
34
-
35
- return next.handle().pipe(tap({ next: observe, error: observe }));
36
- }
37
- }
@@ -1,28 +0,0 @@
1
- import { Module } from '@nestjs/common';
2
- import { APP_INTERCEPTOR } from '@nestjs/core';
3
- import {
4
- makeHistogramProvider,
5
- PrometheusModule
6
- } from '@willsoto/nestjs-prometheus';
7
- import { MetricsController } from '../controllers';
8
- import { MetricsInterceptor } from '../interceptors';
9
-
10
- @Module({
11
- imports: [
12
- PrometheusModule.register({
13
- defaultLabels: { app: 'acme-api' },
14
- defaultMetrics: { enabled: true },
15
- controller: MetricsController
16
- })
17
- ],
18
- providers: [
19
- makeHistogramProvider({
20
- name: 'http_request_duration_seconds',
21
- help: 'Duration of HTTP requests in seconds',
22
- labelNames: ['method', 'route', 'status'],
23
- buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]
24
- }),
25
- { provide: APP_INTERCEPTOR, useClass: MetricsInterceptor }
26
- ]
27
- })
28
- export class MetricsModule {}