@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: load-testing
|
|
3
|
+
description: Load testing patterns with k6 and Artillery for scenarios, thresholds, spike testing, stress testing, and reporting.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Load Testing Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Run load tests before major releases, after infrastructure changes, and regularly in CI to catch performance regressions. Use k6 for script-based testing with built-in metrics, or Artillery for YAML-based declarative scenarios. These patterns cover gradual ramp-up, spike testing, soak testing, and threshold-based pass/fail criteria. Test against staging environments that mirror production, never against production directly.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### k6 Basic Load Test
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// load-tests/api-load.js
|
|
17
|
+
import http from 'k6/http';
|
|
18
|
+
import { check, sleep } from 'k6';
|
|
19
|
+
import { Rate, Trend } from 'k6/metrics';
|
|
20
|
+
|
|
21
|
+
const errorRate = new Rate('errors');
|
|
22
|
+
const latency = new Trend('api_latency');
|
|
23
|
+
|
|
24
|
+
export const options = {
|
|
25
|
+
stages: [
|
|
26
|
+
{ duration: '1m', target: 20 }, // ramp up to 20 VUs
|
|
27
|
+
{ duration: '3m', target: 20 }, // hold at 20 VUs
|
|
28
|
+
{ duration: '2m', target: 50 }, // ramp up to 50 VUs
|
|
29
|
+
{ duration: '3m', target: 50 }, // hold at 50 VUs
|
|
30
|
+
{ duration: '1m', target: 0 }, // ramp down
|
|
31
|
+
],
|
|
32
|
+
thresholds: {
|
|
33
|
+
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95th < 500ms, 99th < 1s
|
|
34
|
+
http_req_failed: ['rate<0.01'], // error rate < 1%
|
|
35
|
+
errors: ['rate<0.05'], // custom error rate < 5%
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
|
|
40
|
+
|
|
41
|
+
export default function () {
|
|
42
|
+
// GET endpoint
|
|
43
|
+
const listRes = http.get(`${BASE_URL}/api/posts`, {
|
|
44
|
+
headers: { Authorization: `Bearer ${__ENV.TOKEN}` },
|
|
45
|
+
tags: { name: 'GET /api/posts' },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
check(listRes, {
|
|
49
|
+
'GET /api/posts status 200': (r) => r.status === 200,
|
|
50
|
+
'GET /api/posts has items': (r) => JSON.parse(r.body).length > 0,
|
|
51
|
+
}) || errorRate.add(1);
|
|
52
|
+
|
|
53
|
+
latency.add(listRes.timings.duration);
|
|
54
|
+
|
|
55
|
+
sleep(1);
|
|
56
|
+
|
|
57
|
+
// POST endpoint
|
|
58
|
+
const createRes = http.post(
|
|
59
|
+
`${BASE_URL}/api/posts`,
|
|
60
|
+
JSON.stringify({ title: `Post ${Date.now()}`, body: 'Load test content' }),
|
|
61
|
+
{
|
|
62
|
+
headers: {
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
Authorization: `Bearer ${__ENV.TOKEN}`,
|
|
65
|
+
},
|
|
66
|
+
tags: { name: 'POST /api/posts' },
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
check(createRes, {
|
|
71
|
+
'POST /api/posts status 201': (r) => r.status === 201,
|
|
72
|
+
}) || errorRate.add(1);
|
|
73
|
+
|
|
74
|
+
sleep(1);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### k6 Spike Test
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
// load-tests/spike-test.js
|
|
82
|
+
export const options = {
|
|
83
|
+
stages: [
|
|
84
|
+
{ duration: '30s', target: 10 }, // warm up
|
|
85
|
+
{ duration: '10s', target: 200 }, // spike to 200 VUs
|
|
86
|
+
{ duration: '1m', target: 200 }, // hold spike
|
|
87
|
+
{ duration: '10s', target: 10 }, // drop back
|
|
88
|
+
{ duration: '2m', target: 10 }, // recover
|
|
89
|
+
{ duration: '30s', target: 0 }, // cool down
|
|
90
|
+
],
|
|
91
|
+
thresholds: {
|
|
92
|
+
http_req_duration: ['p(95)<2000'], // allow higher latency during spike
|
|
93
|
+
http_req_failed: ['rate<0.05'], // allow up to 5% errors during spike
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### k6 Soak Test
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
// load-tests/soak-test.js
|
|
102
|
+
export const options = {
|
|
103
|
+
stages: [
|
|
104
|
+
{ duration: '2m', target: 30 }, // ramp up
|
|
105
|
+
{ duration: '4h', target: 30 }, // hold for 4 hours
|
|
106
|
+
{ duration: '2m', target: 0 }, // ramp down
|
|
107
|
+
],
|
|
108
|
+
thresholds: {
|
|
109
|
+
http_req_duration: ['p(95)<500'],
|
|
110
|
+
http_req_failed: ['rate<0.01'],
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Soak tests catch:
|
|
115
|
+
// - Memory leaks (watch RSS growth)
|
|
116
|
+
// - Connection pool exhaustion
|
|
117
|
+
// - Log file growth
|
|
118
|
+
// - Certificate/token expiry during test
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Artillery YAML Scenario
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
# load-tests/artillery.yml
|
|
125
|
+
config:
|
|
126
|
+
target: "http://localhost:3000"
|
|
127
|
+
phases:
|
|
128
|
+
- duration: 60
|
|
129
|
+
arrivalRate: 5
|
|
130
|
+
name: "Warm up"
|
|
131
|
+
- duration: 180
|
|
132
|
+
arrivalRate: 20
|
|
133
|
+
name: "Sustained load"
|
|
134
|
+
- duration: 60
|
|
135
|
+
arrivalRate: 50
|
|
136
|
+
name: "Peak load"
|
|
137
|
+
defaults:
|
|
138
|
+
headers:
|
|
139
|
+
Content-Type: "application/json"
|
|
140
|
+
plugins:
|
|
141
|
+
expect: {}
|
|
142
|
+
ensure:
|
|
143
|
+
p95: 500
|
|
144
|
+
maxErrorRate: 1
|
|
145
|
+
|
|
146
|
+
scenarios:
|
|
147
|
+
- name: "Browse and create"
|
|
148
|
+
weight: 70
|
|
149
|
+
flow:
|
|
150
|
+
- get:
|
|
151
|
+
url: "/api/posts"
|
|
152
|
+
expect:
|
|
153
|
+
- statusCode: 200
|
|
154
|
+
capture:
|
|
155
|
+
- json: "$[0].id"
|
|
156
|
+
as: "postId"
|
|
157
|
+
- think: 2
|
|
158
|
+
- get:
|
|
159
|
+
url: "/api/posts/{{ postId }}"
|
|
160
|
+
expect:
|
|
161
|
+
- statusCode: 200
|
|
162
|
+
|
|
163
|
+
- name: "Create post"
|
|
164
|
+
weight: 30
|
|
165
|
+
flow:
|
|
166
|
+
- post:
|
|
167
|
+
url: "/api/posts"
|
|
168
|
+
json:
|
|
169
|
+
title: "Load test {{ $randomString() }}"
|
|
170
|
+
body: "Content for load testing"
|
|
171
|
+
expect:
|
|
172
|
+
- statusCode: 201
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Custom k6 Scenario with Lifecycle
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
// load-tests/realistic-scenario.js
|
|
179
|
+
import http from 'k6/http';
|
|
180
|
+
import { check, group, sleep } from 'k6';
|
|
181
|
+
import { SharedArray } from 'k6/data';
|
|
182
|
+
|
|
183
|
+
// Load test data once (shared across VUs)
|
|
184
|
+
const users = new SharedArray('users', function () {
|
|
185
|
+
return JSON.parse(open('./test-users.json'));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
export function setup() {
|
|
189
|
+
// Run once before all VUs
|
|
190
|
+
const res = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
|
|
191
|
+
email: 'admin@test.com',
|
|
192
|
+
password: 'admin123',
|
|
193
|
+
}), { headers: { 'Content-Type': 'application/json' } });
|
|
194
|
+
|
|
195
|
+
return { adminToken: JSON.parse(res.body).token };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default function (data) {
|
|
199
|
+
const user = users[__VU % users.length];
|
|
200
|
+
|
|
201
|
+
group('Authentication', () => {
|
|
202
|
+
const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
|
|
203
|
+
email: user.email,
|
|
204
|
+
password: user.password,
|
|
205
|
+
}), { headers: { 'Content-Type': 'application/json' } });
|
|
206
|
+
|
|
207
|
+
check(loginRes, { 'login successful': (r) => r.status === 200 });
|
|
208
|
+
|
|
209
|
+
const token = JSON.parse(loginRes.body).token;
|
|
210
|
+
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
|
211
|
+
|
|
212
|
+
group('Browse content', () => {
|
|
213
|
+
const posts = http.get(`${BASE_URL}/api/posts`, { headers });
|
|
214
|
+
check(posts, { 'posts loaded': (r) => r.status === 200 });
|
|
215
|
+
sleep(2);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
group('Create content', () => {
|
|
219
|
+
const newPost = http.post(`${BASE_URL}/api/posts`, JSON.stringify({
|
|
220
|
+
title: `VU${__VU} Post ${__ITER}`,
|
|
221
|
+
body: 'Load test generated content',
|
|
222
|
+
}), { headers });
|
|
223
|
+
check(newPost, { 'post created': (r) => r.status === 201 });
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
sleep(Math.random() * 3 + 1); // random think time 1-4s
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function teardown(data) {
|
|
231
|
+
// Cleanup after all VUs complete
|
|
232
|
+
http.del(`${BASE_URL}/api/test/cleanup`, null, {
|
|
233
|
+
headers: { Authorization: `Bearer ${data.adminToken}` },
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### CI Integration and Reporting
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
# .github/workflows/load-test.yml
|
|
242
|
+
name: Load Test
|
|
243
|
+
on:
|
|
244
|
+
schedule:
|
|
245
|
+
- cron: '0 2 * * 1' # weekly Monday 2am
|
|
246
|
+
workflow_dispatch:
|
|
247
|
+
|
|
248
|
+
jobs:
|
|
249
|
+
load-test:
|
|
250
|
+
runs-on: ubuntu-latest
|
|
251
|
+
timeout-minutes: 30
|
|
252
|
+
services:
|
|
253
|
+
app:
|
|
254
|
+
image: myapp:latest
|
|
255
|
+
ports: ['3000:3000']
|
|
256
|
+
steps:
|
|
257
|
+
- uses: actions/checkout@v4
|
|
258
|
+
- uses: grafana/k6-action@v0.3.1
|
|
259
|
+
with:
|
|
260
|
+
filename: load-tests/api-load.js
|
|
261
|
+
flags: --out json=results.json
|
|
262
|
+
env:
|
|
263
|
+
BASE_URL: http://localhost:3000
|
|
264
|
+
TOKEN: ${{ secrets.LOAD_TEST_TOKEN }}
|
|
265
|
+
- uses: actions/upload-artifact@v4
|
|
266
|
+
with:
|
|
267
|
+
name: k6-results
|
|
268
|
+
path: results.json
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Analyzing Results
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# Run k6 with JSON output
|
|
275
|
+
k6 run --out json=results.json load-tests/api-load.js
|
|
276
|
+
|
|
277
|
+
# Run k6 with summary export
|
|
278
|
+
k6 run --summary-export=summary.json load-tests/api-load.js
|
|
279
|
+
|
|
280
|
+
# Run Artillery with HTML report
|
|
281
|
+
npx artillery run load-tests/artillery.yml --output report.json
|
|
282
|
+
npx artillery report report.json --output report.html
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Examples
|
|
286
|
+
|
|
287
|
+
| Test Type | VU Pattern | Purpose |
|
|
288
|
+
|-----------|-----------|---------|
|
|
289
|
+
| Load | 10 to 50 VUs, hold | Normal expected traffic |
|
|
290
|
+
| Stress | Ramp to breaking point | Find maximum capacity |
|
|
291
|
+
| Spike | Sudden jump to 10x | Test auto-scaling and recovery |
|
|
292
|
+
| Soak | Constant 30 VUs for 4h | Memory leaks, resource exhaustion |
|
|
293
|
+
| Breakpoint | Increment until failure | Absolute system limits |
|
|
294
|
+
|
|
295
|
+
## Checklist
|
|
296
|
+
- [ ] Thresholds set for p95 latency, p99 latency, and error rate
|
|
297
|
+
- [ ] Test stages include ramp-up, hold, and ramp-down phases
|
|
298
|
+
- [ ] `check()` validates response status and body content
|
|
299
|
+
- [ ] Test data loaded via `SharedArray` to minimize memory per VU
|
|
300
|
+
- [ ] Think time (`sleep()`) simulates realistic user behavior
|
|
301
|
+
- [ ] Tests run against staging, never production
|
|
302
|
+
- [ ] CI pipeline runs load tests on schedule with artifact upload
|
|
303
|
+
- [ ] Spike and soak tests run before major releases
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: logging-observability
|
|
3
|
+
description: Logging and observability patterns with structured logging, OpenTelemetry, distributed tracing, log levels, and dashboards.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Logging and Observability
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply to every production service. Observability is the ability to understand what your system is doing from its outputs. Use structured logging for searchable logs, OpenTelemetry for distributed tracing, metrics for dashboards and alerts, and correlation IDs to trace requests across services. Set this up before the first outage, not after.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Structured Logging with Pino
|
|
14
|
+
|
|
15
|
+
Never use `console.log` in production. Use structured JSON logs:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import pino from "pino";
|
|
19
|
+
|
|
20
|
+
const logger = pino({
|
|
21
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
22
|
+
formatters: {
|
|
23
|
+
level: (label) => ({ level: label }),
|
|
24
|
+
},
|
|
25
|
+
redact: ["req.headers.authorization", "password", "token"],
|
|
26
|
+
serializers: {
|
|
27
|
+
err: pino.stdSerializers.err,
|
|
28
|
+
req: pino.stdSerializers.req,
|
|
29
|
+
res: pino.stdSerializers.res,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Structured log with context -- every field is searchable
|
|
34
|
+
logger.info(
|
|
35
|
+
{ userId: "u_123", orderId: "o_456", amount: 99.99 },
|
|
36
|
+
"Order created"
|
|
37
|
+
);
|
|
38
|
+
// Output: {"level":"info","time":1716700000,"userId":"u_123","orderId":"o_456",
|
|
39
|
+
// "amount":99.99,"msg":"Order created"}
|
|
40
|
+
|
|
41
|
+
// Child logger inherits context
|
|
42
|
+
const reqLogger = logger.child({ requestId: "req_abc", userId: "u_123" });
|
|
43
|
+
reqLogger.info("Processing payment");
|
|
44
|
+
// All logs from reqLogger include requestId and userId automatically
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Log Levels -- When to Use Each
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
logger.fatal("Database connection lost, shutting down"); // process crash imminent
|
|
51
|
+
logger.error({ err }, "Payment processing failed"); // operation failed, needs attention
|
|
52
|
+
logger.warn("Rate limit approaching: 85% capacity"); // degraded, not broken yet
|
|
53
|
+
logger.info("Order #456 shipped to customer"); // significant business events
|
|
54
|
+
logger.debug({ query, params }, "Executing SQL"); // dev-only detail
|
|
55
|
+
logger.trace({ headers }, "Incoming request headers"); // extremely verbose
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Production runs at `info`. Set `debug` only for targeted troubleshooting.
|
|
59
|
+
|
|
60
|
+
### Request Correlation Middleware
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { randomUUID } from "crypto";
|
|
64
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
65
|
+
import { Request, Response, NextFunction } from "express";
|
|
66
|
+
|
|
67
|
+
interface RequestContext {
|
|
68
|
+
requestId: string;
|
|
69
|
+
userId?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const requestContext = new AsyncLocalStorage<RequestContext>();
|
|
73
|
+
|
|
74
|
+
function correlationMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
75
|
+
const requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
|
|
76
|
+
res.setHeader("x-request-id", requestId);
|
|
77
|
+
|
|
78
|
+
const ctx: RequestContext = { requestId, userId: req.user?.id };
|
|
79
|
+
requestContext.run(ctx, () => {
|
|
80
|
+
const start = Date.now();
|
|
81
|
+
res.on("finish", () => {
|
|
82
|
+
logger.child(ctx).info({
|
|
83
|
+
method: req.method,
|
|
84
|
+
path: req.path,
|
|
85
|
+
statusCode: res.statusCode,
|
|
86
|
+
durationMs: Date.now() - start,
|
|
87
|
+
}, "Request completed");
|
|
88
|
+
});
|
|
89
|
+
next();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get logger anywhere in the call stack
|
|
94
|
+
function getLogger() {
|
|
95
|
+
const ctx = requestContext.getStore();
|
|
96
|
+
return logger.child(ctx ?? {});
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### OpenTelemetry -- Distributed Tracing
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
104
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
105
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
106
|
+
|
|
107
|
+
const sdk = new NodeSDK({
|
|
108
|
+
traceExporter: new OTLPTraceExporter({
|
|
109
|
+
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318/v1/traces",
|
|
110
|
+
}),
|
|
111
|
+
instrumentations: [
|
|
112
|
+
getNodeAutoInstrumentations({
|
|
113
|
+
"@opentelemetry/instrumentation-http": { enabled: true },
|
|
114
|
+
"@opentelemetry/instrumentation-express": { enabled: true },
|
|
115
|
+
"@opentelemetry/instrumentation-pg": { enabled: true },
|
|
116
|
+
}),
|
|
117
|
+
],
|
|
118
|
+
serviceName: "order-service",
|
|
119
|
+
});
|
|
120
|
+
sdk.start();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Custom spans for business logic:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { trace, SpanStatusCode } from "@opentelemetry/api";
|
|
127
|
+
|
|
128
|
+
const tracer = trace.getTracer("order-service");
|
|
129
|
+
|
|
130
|
+
async function processOrder(order: Order) {
|
|
131
|
+
return tracer.startActiveSpan("processOrder", async (span) => {
|
|
132
|
+
span.setAttribute("order.id", order.id);
|
|
133
|
+
span.setAttribute("order.total", order.total);
|
|
134
|
+
try {
|
|
135
|
+
await validateOrder(order);
|
|
136
|
+
await chargePayment(order);
|
|
137
|
+
await sendConfirmation(order);
|
|
138
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
139
|
+
} catch (err: any) {
|
|
140
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
141
|
+
span.recordException(err);
|
|
142
|
+
throw err;
|
|
143
|
+
} finally {
|
|
144
|
+
span.end();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Prometheus Metrics
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { Counter, Histogram, Registry } from "prom-client";
|
|
154
|
+
|
|
155
|
+
const registry = new Registry();
|
|
156
|
+
|
|
157
|
+
const httpDuration = new Histogram({
|
|
158
|
+
name: "http_request_duration_seconds",
|
|
159
|
+
help: "HTTP request duration in seconds",
|
|
160
|
+
labelNames: ["method", "route", "status"] as const,
|
|
161
|
+
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
|
|
162
|
+
registers: [registry],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const httpTotal = new Counter({
|
|
166
|
+
name: "http_requests_total",
|
|
167
|
+
help: "Total HTTP requests",
|
|
168
|
+
labelNames: ["method", "route", "status"] as const,
|
|
169
|
+
registers: [registry],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
function metricsMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
173
|
+
const end = httpDuration.startTimer();
|
|
174
|
+
res.on("finish", () => {
|
|
175
|
+
const labels = {
|
|
176
|
+
method: req.method,
|
|
177
|
+
route: req.route?.path ?? req.path,
|
|
178
|
+
status: String(res.statusCode),
|
|
179
|
+
};
|
|
180
|
+
end(labels);
|
|
181
|
+
httpTotal.inc(labels);
|
|
182
|
+
});
|
|
183
|
+
next();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
app.get("/metrics", async (_req, res) => {
|
|
187
|
+
res.set("Content-Type", registry.contentType);
|
|
188
|
+
res.end(await registry.metrics());
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Health Check Endpoint
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
app.get("/health", async (_req, res) => {
|
|
196
|
+
const checks = {
|
|
197
|
+
database: await checkDatabase().catch(() => false),
|
|
198
|
+
redis: await checkRedis().catch(() => false),
|
|
199
|
+
uptime: process.uptime(),
|
|
200
|
+
memoryMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
201
|
+
};
|
|
202
|
+
const healthy = checks.database && checks.redis;
|
|
203
|
+
res.status(healthy ? 200 : 503).json({
|
|
204
|
+
status: healthy ? "ok" : "degraded",
|
|
205
|
+
checks,
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Examples
|
|
211
|
+
|
|
212
|
+
| Signal | Tool | Use Case |
|
|
213
|
+
|--------|------|----------|
|
|
214
|
+
| Structured logs | Pino + ELK/Datadog | Search and filter by any field |
|
|
215
|
+
| Distributed traces | OpenTelemetry + Jaeger | Follow request across services |
|
|
216
|
+
| Metrics | Prometheus + Grafana | Dashboards, SLO tracking, alerts |
|
|
217
|
+
| Correlation IDs | AsyncLocalStorage | Link logs per request |
|
|
218
|
+
| Health checks | /health endpoint | Load balancer, K8s probes |
|
|
219
|
+
|
|
220
|
+
## Checklist
|
|
221
|
+
- [ ] All logs are structured JSON, not string concatenation
|
|
222
|
+
- [ ] Sensitive data redacted from logs (tokens, passwords, PII)
|
|
223
|
+
- [ ] Correlation ID propagated across all service calls
|
|
224
|
+
- [ ] Log levels used correctly (fatal/error/warn/info/debug)
|
|
225
|
+
- [ ] OpenTelemetry traces span critical business operations
|
|
226
|
+
- [ ] Prometheus metrics track duration, error rate, throughput
|
|
227
|
+
- [ ] Health check endpoint reports dependency status
|
|
228
|
+
- [ ] Alerts configured for error rate and p99 latency thresholds
|