@garethdaine/agentops 0.9.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/.claude-plugin/plugin.json +10 -0
- package/LICENSE +21 -0
- package/README.md +410 -0
- package/agents/architecture-researcher.md +115 -0
- package/agents/code-critic.md +190 -0
- package/agents/delegation-router.md +40 -0
- package/agents/feature-researcher.md +117 -0
- package/agents/interrogator.md +11 -0
- package/agents/pitfalls-researcher.md +112 -0
- package/agents/plan-validator.md +173 -0
- package/agents/proposer.md +61 -0
- package/agents/security-reviewer.md +189 -0
- package/agents/skill-builder.md +43 -0
- package/agents/spec-compliance-reviewer.md +154 -0
- package/agents/stack-researcher.md +89 -0
- package/commands/build.md +766 -0
- package/commands/code-analysis.md +39 -0
- package/commands/code-field.md +22 -0
- package/commands/compliance-check.md +34 -0
- package/commands/configure.md +178 -0
- package/commands/cost-report.md +17 -0
- package/commands/enterprise/adr.md +78 -0
- package/commands/enterprise/brainstorm.md +461 -0
- package/commands/enterprise/design.md +203 -0
- package/commands/enterprise/dev-setup.md +136 -0
- package/commands/enterprise/docker-dev.md +229 -0
- package/commands/enterprise/e2e.md +233 -0
- package/commands/enterprise/feature.md +218 -0
- package/commands/enterprise/gap-analysis.md +204 -0
- package/commands/enterprise/handover.md +195 -0
- package/commands/enterprise/herd.md +152 -0
- package/commands/enterprise/knowledge.md +173 -0
- package/commands/enterprise/onboard.md +86 -0
- package/commands/enterprise/qa-check.md +80 -0
- package/commands/enterprise/reason.md +196 -0
- package/commands/enterprise/review.md +177 -0
- package/commands/enterprise/scaffold.md +153 -0
- package/commands/enterprise/status-report.md +101 -0
- package/commands/enterprise/tech-catalog.md +170 -0
- package/commands/enterprise/test-gen.md +138 -0
- package/commands/evolve.md +39 -0
- package/commands/flags.md +44 -0
- package/commands/interrogate.md +263 -0
- package/commands/lesson.md +15 -0
- package/commands/lessons.md +10 -0
- package/commands/plan.md +44 -0
- package/commands/prune.md +27 -0
- package/commands/star.md +17 -0
- package/commands/supply-chain-scan.md +44 -0
- package/commands/unicode-scan.md +63 -0
- package/commands/verify.md +41 -0
- package/commands/workflow.md +436 -0
- package/hooks/ai-guardrails.sh +114 -0
- package/hooks/audit-log.sh +26 -0
- package/hooks/auto-delegate.sh +45 -0
- package/hooks/auto-evolve.sh +22 -0
- package/hooks/auto-lesson.sh +26 -0
- package/hooks/auto-plan.sh +59 -0
- package/hooks/auto-test.sh +46 -0
- package/hooks/auto-verify.sh +30 -0
- package/hooks/budget-check.sh +24 -0
- package/hooks/code-field-preamble.sh +30 -0
- package/hooks/compliance-gate.sh +50 -0
- package/hooks/content-trust.sh +22 -0
- package/hooks/credential-redact.sh +23 -0
- package/hooks/delegation-trust.sh +15 -0
- package/hooks/detect-test-run.sh +19 -0
- package/hooks/enforcement-lib.sh +60 -0
- package/hooks/evolve-gate.sh +32 -0
- package/hooks/evolve-lib.sh +32 -0
- package/hooks/exfiltration-check.sh +67 -0
- package/hooks/failure-collector.sh +27 -0
- package/hooks/feature-flags.sh +67 -0
- package/hooks/file-provenance.sh +31 -0
- package/hooks/flag-utils.sh +36 -0
- package/hooks/hooks.json +145 -0
- package/hooks/injection-scan.sh +58 -0
- package/hooks/integrity-verify.sh +91 -0
- package/hooks/lessons-check.sh +17 -0
- package/hooks/lockfile-audit.sh +109 -0
- package/hooks/patterns-lib.sh +22 -0
- package/hooks/plan-gate.sh +18 -0
- package/hooks/redact-lib.sh +15 -0
- package/hooks/runtime-mode.sh +56 -0
- package/hooks/session-cleanup.sh +74 -0
- package/hooks/skill-validator.sh +28 -0
- package/hooks/standards-enforce.sh +106 -0
- package/hooks/star-gate.sh +93 -0
- package/hooks/star-preamble.sh +10 -0
- package/hooks/telemetry.sh +33 -0
- package/hooks/todo-prune.sh +84 -0
- package/hooks/unicode-firewall.sh +122 -0
- package/hooks/unicode-lib.sh +66 -0
- package/hooks/unicode-scan-session.sh +96 -0
- package/hooks/validate-command.sh +103 -0
- package/hooks/validate-env.sh +51 -0
- package/hooks/validate-path.sh +81 -0
- package/package.json +40 -0
- package/settings.json +6 -0
- package/templates/ai-config/tool-standards.md +56 -0
- package/templates/architecture/api-first.md +192 -0
- package/templates/architecture/auth-patterns.md +302 -0
- package/templates/architecture/caching-strategy.md +359 -0
- package/templates/architecture/database-patterns.md +347 -0
- package/templates/architecture/event-driven.md +252 -0
- package/templates/architecture/integration-patterns.md +185 -0
- package/templates/architecture/multi-tenancy.md +104 -0
- package/templates/architecture/service-boundaries.md +200 -0
- package/templates/build/brief-template.md +86 -0
- package/templates/build/summary-template.md +100 -0
- package/templates/build/task-plan-template.md +133 -0
- package/templates/communication/effort-estimate.md +54 -0
- package/templates/communication/incident-response.md +59 -0
- package/templates/communication/post-mortem.md +109 -0
- package/templates/communication/risk-register.md +43 -0
- package/templates/communication/sprint-demo-checklist.md +64 -0
- package/templates/communication/stakeholder-presentation-outline.md +84 -0
- package/templates/communication/technical-proposal.md +77 -0
- package/templates/delivery/deployment/deployment-checklist.md +49 -0
- package/templates/delivery/design/solution-design-checklist.md +37 -0
- package/templates/delivery/discovery/stakeholder-questions.md +33 -0
- package/templates/delivery/handover/knowledge-transfer-checklist.md +75 -0
- package/templates/delivery/handover/operational-runbook.md +117 -0
- package/templates/delivery/handover/support-escalation-matrix.md +56 -0
- package/templates/delivery/implementation/blocker-escalation-template.md +55 -0
- package/templates/delivery/implementation/sprint-planning-template.md +49 -0
- package/templates/delivery/implementation/task-decomposition-guide.md +59 -0
- package/templates/delivery/qa/test-plan-template.md +76 -0
- package/templates/delivery/qa/test-results-template.md +55 -0
- package/templates/delivery/qa/uat-signoff-template.md +44 -0
- package/templates/governance/codeowners.md +60 -0
- package/templates/integration/adapter-pattern.md +160 -0
- package/templates/scaffolds/env-validation.md +85 -0
- package/templates/scaffolds/error-handling.md +171 -0
- package/templates/scaffolds/graceful-shutdown.md +139 -0
- package/templates/scaffolds/health-check.md +109 -0
- package/templates/scaffolds/structured-logging.md +134 -0
- package/templates/standards/engineering-standards.md +413 -0
- package/templates/standards/standards-checklist.md +125 -0
- package/templates/tech-catalog.json +663 -0
- package/templates/utilities/project-detection.md +75 -0
- package/templates/utilities/requirements-collection.md +68 -0
- package/templates/utilities/template-rendering.md +81 -0
- package/templates/workflows/architecture-decision.md +90 -0
- package/templates/workflows/bug-investigation.md +83 -0
- package/templates/workflows/feature-implementation.md +80 -0
- package/templates/workflows/refactoring.md +83 -0
- package/templates/workflows/spike-exploration.md +82 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Enterprise Pattern: Environment Validation
|
|
2
|
+
|
|
3
|
+
Generate the following environment validation pattern adapted to the project's chosen stack.
|
|
4
|
+
|
|
5
|
+
## Environment Schema (`src/lib/env.ts`)
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Define all required environment variables with types and defaults.
|
|
12
|
+
* The app will fail fast at startup if any required variable is missing or invalid.
|
|
13
|
+
*/
|
|
14
|
+
const envSchema = z.object({
|
|
15
|
+
// App
|
|
16
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
17
|
+
PORT: z.coerce.number().default(3000),
|
|
18
|
+
HOST: z.string().default('0.0.0.0'),
|
|
19
|
+
SERVICE_NAME: z.string().default('{{project_name}}'),
|
|
20
|
+
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error', 'fatal']).default('info'),
|
|
21
|
+
|
|
22
|
+
// {{#if database}}
|
|
23
|
+
// Database
|
|
24
|
+
DATABASE_URL: z.string().url(),
|
|
25
|
+
// {{/if}}
|
|
26
|
+
|
|
27
|
+
// {{#if auth_strategy}}
|
|
28
|
+
// Auth
|
|
29
|
+
// AUTH_SECRET: z.string().min(32),
|
|
30
|
+
// {{/if}}
|
|
31
|
+
|
|
32
|
+
// Add project-specific variables below
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type Env = z.infer<typeof envSchema>;
|
|
36
|
+
|
|
37
|
+
function validateEnv(): Env {
|
|
38
|
+
const result = envSchema.safeParse(process.env);
|
|
39
|
+
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
console.error('Environment validation failed:');
|
|
42
|
+
for (const issue of result.error.issues) {
|
|
43
|
+
console.error(` ${issue.path.join('.')}: ${issue.message}`);
|
|
44
|
+
}
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result.data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validated environment variables — import this instead of using process.env directly.
|
|
53
|
+
* Guarantees type safety and presence of all required variables.
|
|
54
|
+
*/
|
|
55
|
+
export const env = validateEnv();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## .env.example Template
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Application
|
|
62
|
+
NODE_ENV=development
|
|
63
|
+
PORT=3000
|
|
64
|
+
HOST=0.0.0.0
|
|
65
|
+
SERVICE_NAME={{project_name}}
|
|
66
|
+
LOG_LEVEL=debug
|
|
67
|
+
|
|
68
|
+
# Database (uncomment if database selected)
|
|
69
|
+
# DATABASE_URL=postgresql://user:password@localhost:5432/{{project_name}}?schema=public
|
|
70
|
+
|
|
71
|
+
# Authentication (uncomment if auth selected)
|
|
72
|
+
# AUTH_SECRET=generate-a-secret-at-least-32-chars-long
|
|
73
|
+
# AUTH_URL=http://localhost:3000
|
|
74
|
+
|
|
75
|
+
# External Services
|
|
76
|
+
# API_KEY=your-api-key-here
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Usage Notes
|
|
80
|
+
|
|
81
|
+
- Import `env` from `@/lib/env` instead of accessing `process.env` directly
|
|
82
|
+
- Add new environment variables to both the zod schema AND `.env.example`
|
|
83
|
+
- The schema validates at import time — if validation fails, the app exits immediately with clear error messages
|
|
84
|
+
- Use `z.coerce.number()` for numeric env vars (they're always strings in `process.env`)
|
|
85
|
+
- Mark optional variables with `.optional()` or `.default()`
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Enterprise Pattern: Structured Error Handling
|
|
2
|
+
|
|
3
|
+
Generate the following error handling pattern adapted to the project's chosen stack.
|
|
4
|
+
|
|
5
|
+
## Core Error Classes (`src/lib/errors.ts`)
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
/**
|
|
9
|
+
* Base application error with structured metadata.
|
|
10
|
+
* All custom errors extend this class for consistent handling.
|
|
11
|
+
*/
|
|
12
|
+
export class AppError extends Error {
|
|
13
|
+
public readonly statusCode: number;
|
|
14
|
+
public readonly code: string;
|
|
15
|
+
public readonly isOperational: boolean;
|
|
16
|
+
public readonly details?: Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
message: string,
|
|
20
|
+
options: {
|
|
21
|
+
statusCode?: number;
|
|
22
|
+
code?: string;
|
|
23
|
+
isOperational?: boolean;
|
|
24
|
+
details?: Record<string, unknown>;
|
|
25
|
+
cause?: Error;
|
|
26
|
+
} = {},
|
|
27
|
+
) {
|
|
28
|
+
super(message, { cause: options.cause });
|
|
29
|
+
this.name = this.constructor.name;
|
|
30
|
+
this.statusCode = options.statusCode ?? 500;
|
|
31
|
+
this.code = options.code ?? 'INTERNAL_ERROR';
|
|
32
|
+
this.isOperational = options.isOperational ?? true;
|
|
33
|
+
this.details = options.details;
|
|
34
|
+
Error.captureStackTrace(this, this.constructor);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toJSON() {
|
|
38
|
+
return {
|
|
39
|
+
error: {
|
|
40
|
+
code: this.code,
|
|
41
|
+
message: this.message,
|
|
42
|
+
...(this.details && { details: this.details }),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class ValidationError extends AppError {
|
|
49
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
50
|
+
super(message, { statusCode: 400, code: 'VALIDATION_ERROR', details });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class NotFoundError extends AppError {
|
|
55
|
+
constructor(resource: string, id?: string) {
|
|
56
|
+
super(id ? `${resource} with id '${id}' not found` : `${resource} not found`, {
|
|
57
|
+
statusCode: 404,
|
|
58
|
+
code: 'NOT_FOUND',
|
|
59
|
+
details: { resource, ...(id && { id }) },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class AuthenticationError extends AppError {
|
|
65
|
+
constructor(message = 'Authentication required') {
|
|
66
|
+
super(message, { statusCode: 401, code: 'UNAUTHENTICATED' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class AuthorisationError extends AppError {
|
|
71
|
+
constructor(message = 'Insufficient permissions') {
|
|
72
|
+
super(message, { statusCode: 403, code: 'FORBIDDEN' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class ConflictError extends AppError {
|
|
77
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
78
|
+
super(message, { statusCode: 409, code: 'CONFLICT', details });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class RateLimitError extends AppError {
|
|
83
|
+
constructor(retryAfterSeconds?: number) {
|
|
84
|
+
super('Too many requests', {
|
|
85
|
+
statusCode: 429,
|
|
86
|
+
code: 'RATE_LIMITED',
|
|
87
|
+
details: retryAfterSeconds ? { retryAfter: retryAfterSeconds } : undefined,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API Error Response Format
|
|
94
|
+
|
|
95
|
+
All API errors return this consistent structure:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"error": {
|
|
100
|
+
"code": "VALIDATION_ERROR",
|
|
101
|
+
"message": "Email address is invalid",
|
|
102
|
+
"details": {
|
|
103
|
+
"field": "email",
|
|
104
|
+
"value": "not-an-email"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Error Handling Middleware (Express/Fastify/Hono)
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Adapt to the chosen framework's middleware pattern
|
|
114
|
+
export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
|
|
115
|
+
if (err instanceof AppError) {
|
|
116
|
+
logger.warn({ err, requestId: req.id }, err.message);
|
|
117
|
+
return res.status(err.statusCode).json(err.toJSON());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Unexpected errors — log full stack, return generic message
|
|
121
|
+
logger.error({ err, requestId: req.id }, 'Unhandled error');
|
|
122
|
+
return res.status(500).json({
|
|
123
|
+
error: {
|
|
124
|
+
code: 'INTERNAL_ERROR',
|
|
125
|
+
message: 'An unexpected error occurred',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## React Error Boundary (if frontend)
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
'use client';
|
|
135
|
+
|
|
136
|
+
import { Component, type ReactNode } from 'react';
|
|
137
|
+
|
|
138
|
+
interface Props {
|
|
139
|
+
children: ReactNode;
|
|
140
|
+
fallback?: ReactNode;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface State {
|
|
144
|
+
hasError: boolean;
|
|
145
|
+
error?: Error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
149
|
+
state: State = { hasError: false };
|
|
150
|
+
|
|
151
|
+
static getDerivedStateFromError(error: Error): State {
|
|
152
|
+
return { hasError: true, error };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
156
|
+
console.error('ErrorBoundary caught:', error, info);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
render() {
|
|
160
|
+
if (this.state.hasError) {
|
|
161
|
+
return this.props.fallback ?? (
|
|
162
|
+
<div role="alert">
|
|
163
|
+
<h2>Something went wrong</h2>
|
|
164
|
+
<p>{this.state.error?.message}</p>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return this.props.children;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Enterprise Pattern: Graceful Shutdown
|
|
2
|
+
|
|
3
|
+
Generate the following graceful shutdown pattern adapted to the project's chosen framework.
|
|
4
|
+
|
|
5
|
+
## Shutdown Handler (`src/lib/shutdown.ts`)
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { logger } from '@/lib/logger';
|
|
9
|
+
|
|
10
|
+
type CleanupFn = () => Promise<void> | void;
|
|
11
|
+
|
|
12
|
+
const cleanupHandlers: Array<{ name: string; fn: CleanupFn }> = [];
|
|
13
|
+
let isShuttingDown = false;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a cleanup function to run during graceful shutdown.
|
|
17
|
+
* Handlers run in reverse registration order (LIFO).
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* registerCleanup('database', async () => {
|
|
21
|
+
* await prisma.$disconnect();
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* registerCleanup('http-server', async () => {
|
|
25
|
+
* await new Promise<void>((resolve) => server.close(resolve));
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
export function registerCleanup(name: string, fn: CleanupFn): void {
|
|
29
|
+
cleanupHandlers.push({ name, fn });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Execute graceful shutdown. Called automatically on SIGTERM/SIGINT.
|
|
34
|
+
* Runs all cleanup handlers with a timeout to prevent hanging.
|
|
35
|
+
*/
|
|
36
|
+
async function shutdown(signal: string): Promise<void> {
|
|
37
|
+
if (isShuttingDown) return;
|
|
38
|
+
isShuttingDown = true;
|
|
39
|
+
|
|
40
|
+
logger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
41
|
+
|
|
42
|
+
const SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
43
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
44
|
+
setTimeout(() => reject(new Error('Shutdown timed out')), SHUTDOWN_TIMEOUT_MS),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Run handlers in reverse order (LIFO)
|
|
49
|
+
const handlers = [...cleanupHandlers].reverse();
|
|
50
|
+
|
|
51
|
+
await Promise.race([
|
|
52
|
+
(async () => {
|
|
53
|
+
for (const handler of handlers) {
|
|
54
|
+
try {
|
|
55
|
+
logger.info(`Cleaning up: ${handler.name}`);
|
|
56
|
+
await handler.fn();
|
|
57
|
+
logger.info(`Cleaned up: ${handler.name}`);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.error(`Cleanup failed: ${handler.name}`, {
|
|
60
|
+
error: error instanceof Error ? error.message : String(error),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})(),
|
|
65
|
+
timeoutPromise,
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
logger.info('Graceful shutdown complete');
|
|
69
|
+
process.exit(0);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error('Shutdown timed out, forcing exit', {
|
|
72
|
+
error: error instanceof Error ? error.message : String(error),
|
|
73
|
+
});
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Install signal handlers. Call this once at application startup.
|
|
80
|
+
*/
|
|
81
|
+
export function installShutdownHandlers(): void {
|
|
82
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
83
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
84
|
+
|
|
85
|
+
// Handle uncaught exceptions — log and exit
|
|
86
|
+
process.on('uncaughtException', (error) => {
|
|
87
|
+
logger.fatal('Uncaught exception', { error: error.message, stack: error.stack });
|
|
88
|
+
shutdown('uncaughtException');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Handle unhandled promise rejections
|
|
92
|
+
process.on('unhandledRejection', (reason) => {
|
|
93
|
+
logger.fatal('Unhandled rejection', {
|
|
94
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
95
|
+
});
|
|
96
|
+
shutdown('unhandledRejection');
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Server Entry Point Integration
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { installShutdownHandlers, registerCleanup } from '@/lib/shutdown';
|
|
105
|
+
import { logger } from '@/lib/logger';
|
|
106
|
+
import { env } from '@/lib/env';
|
|
107
|
+
|
|
108
|
+
// Install signal handlers first
|
|
109
|
+
installShutdownHandlers();
|
|
110
|
+
|
|
111
|
+
// Start server
|
|
112
|
+
const server = app.listen(env.PORT, env.HOST, () => {
|
|
113
|
+
logger.info(`Server started`, { port: env.PORT, host: env.HOST, env: env.NODE_ENV });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Register cleanup: close HTTP server (stop accepting new connections, drain existing)
|
|
117
|
+
registerCleanup('http-server', async () => {
|
|
118
|
+
await new Promise<void>((resolve, reject) => {
|
|
119
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Register cleanup: disconnect database
|
|
124
|
+
registerCleanup('database', async () => {
|
|
125
|
+
// Adapt to chosen ORM:
|
|
126
|
+
// Prisma: await prisma.$disconnect();
|
|
127
|
+
// Drizzle: await pool.end();
|
|
128
|
+
// TypeORM: await dataSource.destroy();
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Usage Notes
|
|
133
|
+
|
|
134
|
+
- Always install shutdown handlers at the very start of the application entry point
|
|
135
|
+
- Register cleanup handlers immediately after creating resources (server, DB connections, etc.)
|
|
136
|
+
- LIFO order ensures the HTTP server stops accepting connections before database disconnects
|
|
137
|
+
- The 30-second timeout prevents infinite hangs from stuck connections
|
|
138
|
+
- Docker sends SIGTERM first, then SIGKILL after the grace period (default 10s, configure with `stop_grace_period`)
|
|
139
|
+
- In Kubernetes, set `terminationGracePeriodSeconds` to match or exceed the shutdown timeout
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Enterprise Pattern: Health Check Endpoints
|
|
2
|
+
|
|
3
|
+
Generate the following health check pattern adapted to the project's chosen framework.
|
|
4
|
+
|
|
5
|
+
## Health Check Routes
|
|
6
|
+
|
|
7
|
+
### Liveness Check (`/health`)
|
|
8
|
+
|
|
9
|
+
Returns 200 if the process is alive. No dependency checks — used by load balancers and container orchestrators for basic liveness.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// GET /health
|
|
13
|
+
export function healthHandler(req: Request, res: Response) {
|
|
14
|
+
res.status(200).json({
|
|
15
|
+
status: 'ok',
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
service: env.SERVICE_NAME,
|
|
18
|
+
uptime: process.uptime(),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Readiness Check (`/health/ready`)
|
|
24
|
+
|
|
25
|
+
Returns 200 only if all dependencies are reachable. Used by orchestrators to determine if the service can accept traffic.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
interface HealthComponent {
|
|
29
|
+
name: string;
|
|
30
|
+
status: 'healthy' | 'unhealthy' | 'degraded';
|
|
31
|
+
responseTimeMs?: number;
|
|
32
|
+
details?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function checkDatabase(): Promise<HealthComponent> {
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
try {
|
|
38
|
+
// Adapt to chosen ORM/database driver
|
|
39
|
+
// Prisma: await prisma.$queryRaw`SELECT 1`
|
|
40
|
+
// Drizzle: await db.execute(sql`SELECT 1`)
|
|
41
|
+
// Raw: await pool.query('SELECT 1')
|
|
42
|
+
return {
|
|
43
|
+
name: 'database',
|
|
44
|
+
status: 'healthy',
|
|
45
|
+
responseTimeMs: Date.now() - start,
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
name: 'database',
|
|
50
|
+
status: 'unhealthy',
|
|
51
|
+
responseTimeMs: Date.now() - start,
|
|
52
|
+
details: { error: error instanceof Error ? error.message : 'Unknown error' },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add more dependency checks as needed:
|
|
58
|
+
// async function checkRedis(): Promise<HealthComponent> { ... }
|
|
59
|
+
// async function checkExternalApi(): Promise<HealthComponent> { ... }
|
|
60
|
+
|
|
61
|
+
// GET /health/ready
|
|
62
|
+
export async function readinessHandler(req: Request, res: Response) {
|
|
63
|
+
const checks = await Promise.all([
|
|
64
|
+
checkDatabase(),
|
|
65
|
+
// checkRedis(),
|
|
66
|
+
// checkExternalApi(),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const allHealthy = checks.every((c) => c.status === 'healthy');
|
|
70
|
+
const hasDegraded = checks.some((c) => c.status === 'degraded');
|
|
71
|
+
|
|
72
|
+
const overallStatus = allHealthy ? 'ok' : hasDegraded ? 'degraded' : 'unavailable';
|
|
73
|
+
const statusCode = allHealthy ? 200 : hasDegraded ? 200 : 503;
|
|
74
|
+
|
|
75
|
+
res.status(statusCode).json({
|
|
76
|
+
status: overallStatus,
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
service: env.SERVICE_NAME,
|
|
79
|
+
uptime: process.uptime(),
|
|
80
|
+
components: checks,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Response Format
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"status": "ok",
|
|
90
|
+
"timestamp": "2026-03-17T14:00:00.000Z",
|
|
91
|
+
"service": "acme-portal",
|
|
92
|
+
"uptime": 3600.5,
|
|
93
|
+
"components": [
|
|
94
|
+
{
|
|
95
|
+
"name": "database",
|
|
96
|
+
"status": "healthy",
|
|
97
|
+
"responseTimeMs": 2
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Usage Notes
|
|
104
|
+
|
|
105
|
+
- `/health` should be fast and dependency-free — never add database checks to liveness
|
|
106
|
+
- `/health/ready` should check ALL critical dependencies
|
|
107
|
+
- Set appropriate timeouts on dependency checks (2-5 seconds max)
|
|
108
|
+
- In Kubernetes: use `/health` for `livenessProbe` and `/health/ready` for `readinessProbe`
|
|
109
|
+
- Consider adding a `/health/startup` for slow-starting services
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Enterprise Pattern: Structured JSON Logging
|
|
2
|
+
|
|
3
|
+
Generate the following logging pattern adapted to the project's chosen stack.
|
|
4
|
+
|
|
5
|
+
## Logger Module (`src/lib/logger.ts`)
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
11
|
+
|
|
12
|
+
interface LogEntry {
|
|
13
|
+
level: LogLevel;
|
|
14
|
+
message: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
correlationId?: string;
|
|
17
|
+
requestId?: string;
|
|
18
|
+
userId?: string;
|
|
19
|
+
tenantId?: string;
|
|
20
|
+
service: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
25
|
+
debug: 10,
|
|
26
|
+
info: 20,
|
|
27
|
+
warn: 30,
|
|
28
|
+
error: 40,
|
|
29
|
+
fatal: 50,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const currentLevel = (process.env.LOG_LEVEL as LogLevel) ?? 'info';
|
|
33
|
+
|
|
34
|
+
function shouldLog(level: LogLevel): boolean {
|
|
35
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatEntry(level: LogLevel, message: string, context?: Record<string, unknown>): string {
|
|
39
|
+
const entry: LogEntry = {
|
|
40
|
+
level,
|
|
41
|
+
message,
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
service: process.env.SERVICE_NAME ?? '{{project_name}}',
|
|
44
|
+
...context,
|
|
45
|
+
};
|
|
46
|
+
return JSON.stringify(entry);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const logger = {
|
|
50
|
+
debug(message: string, context?: Record<string, unknown>) {
|
|
51
|
+
if (shouldLog('debug')) console.debug(formatEntry('debug', message, context));
|
|
52
|
+
},
|
|
53
|
+
info(message: string, context?: Record<string, unknown>) {
|
|
54
|
+
if (shouldLog('info')) console.info(formatEntry('info', message, context));
|
|
55
|
+
},
|
|
56
|
+
warn(message: string, context?: Record<string, unknown>) {
|
|
57
|
+
if (shouldLog('warn')) console.warn(formatEntry('warn', message, context));
|
|
58
|
+
},
|
|
59
|
+
error(message: string, context?: Record<string, unknown>) {
|
|
60
|
+
if (shouldLog('error')) console.error(formatEntry('error', message, context));
|
|
61
|
+
},
|
|
62
|
+
fatal(message: string, context?: Record<string, unknown>) {
|
|
63
|
+
if (shouldLog('fatal')) console.error(formatEntry('fatal', message, context));
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a child logger with pre-bound context (e.g., per-request).
|
|
68
|
+
*/
|
|
69
|
+
child(defaultContext: Record<string, unknown>) {
|
|
70
|
+
return {
|
|
71
|
+
debug: (msg: string, ctx?: Record<string, unknown>) =>
|
|
72
|
+
logger.debug(msg, { ...defaultContext, ...ctx }),
|
|
73
|
+
info: (msg: string, ctx?: Record<string, unknown>) =>
|
|
74
|
+
logger.info(msg, { ...defaultContext, ...ctx }),
|
|
75
|
+
warn: (msg: string, ctx?: Record<string, unknown>) =>
|
|
76
|
+
logger.warn(msg, { ...defaultContext, ...ctx }),
|
|
77
|
+
error: (msg: string, ctx?: Record<string, unknown>) =>
|
|
78
|
+
logger.error(msg, { ...defaultContext, ...ctx }),
|
|
79
|
+
fatal: (msg: string, ctx?: Record<string, unknown>) =>
|
|
80
|
+
logger.fatal(msg, { ...defaultContext, ...ctx }),
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate a correlation ID for request tracing.
|
|
87
|
+
*/
|
|
88
|
+
export function generateCorrelationId(): string {
|
|
89
|
+
return randomUUID();
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Request Logging Middleware
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { generateCorrelationId, logger } from '@/lib/logger';
|
|
97
|
+
|
|
98
|
+
// Adapt to the chosen framework's middleware pattern
|
|
99
|
+
export function requestLogger(req: Request, res: Response, next: NextFunction) {
|
|
100
|
+
const correlationId = (req.headers['x-correlation-id'] as string) ?? generateCorrelationId();
|
|
101
|
+
const requestId = generateCorrelationId();
|
|
102
|
+
const start = Date.now();
|
|
103
|
+
|
|
104
|
+
// Attach to request for downstream use
|
|
105
|
+
req.correlationId = correlationId;
|
|
106
|
+
req.requestId = requestId;
|
|
107
|
+
|
|
108
|
+
// Set response header for client correlation
|
|
109
|
+
res.setHeader('x-correlation-id', correlationId);
|
|
110
|
+
res.setHeader('x-request-id', requestId);
|
|
111
|
+
|
|
112
|
+
res.on('finish', () => {
|
|
113
|
+
const duration = Date.now() - start;
|
|
114
|
+
logger.info('request completed', {
|
|
115
|
+
correlationId,
|
|
116
|
+
requestId,
|
|
117
|
+
method: req.method,
|
|
118
|
+
path: req.path,
|
|
119
|
+
statusCode: res.statusCode,
|
|
120
|
+
durationMs: duration,
|
|
121
|
+
userAgent: req.headers['user-agent'],
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
next();
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Usage Notes
|
|
130
|
+
|
|
131
|
+
- In production, pipe stdout to a log aggregator (Datadog, CloudWatch, etc.)
|
|
132
|
+
- Use `logger.child()` to bind request context once, then log throughout the request lifecycle
|
|
133
|
+
- Always include `correlationId` for distributed tracing across services
|
|
134
|
+
- Never log sensitive data (passwords, tokens, PII) — use redaction middleware if needed
|