runward 0.6.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/LICENSE +21 -0
- package/NOTICE.md +13 -0
- package/README.md +128 -0
- package/dist/cli.js +76 -0
- package/dist/commands/check.js +50 -0
- package/dist/commands/doctor.js +75 -0
- package/dist/commands/init.js +90 -0
- package/dist/commands/status.js +49 -0
- package/dist/commands/update.js +48 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/mission.js +96 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/styles.js +60 -0
- package/dist/lib/tools.js +59 -0
- package/dist/lib/write.js +30 -0
- package/package.json +29 -0
- package/templates/mission/adr/ADR-0000-template.md +38 -0
- package/templates/mission/architecture.md +60 -0
- package/templates/mission/decision-matrix.md +37 -0
- package/templates/mission/evaluation-rubric.md +60 -0
- package/templates/mission/floor.md +47 -0
- package/templates/mission/framing.md +58 -0
- package/templates/mission/mission-contract.md +58 -0
- package/templates/mission/observability-schema.md +53 -0
- package/templates/mission/port-contract.md +63 -0
- package/templates/mission/reference-stack.md +70 -0
- package/templates/mission/runbook.md +70 -0
- package/templates/mission/shared-bricks.md +74 -0
- package/templates/mission/threat-model.md +53 -0
- package/templates/rules/async-job-guardrails.md +95 -0
- package/templates/rules/async-post-turn-pipeline.md +96 -0
- package/templates/rules/async-scheduled-maintenance.md +95 -0
- package/templates/rules/cache-three-tier-architecture.md +44 -0
- package/templates/rules/checklist-day-zero-project.md +97 -0
- package/templates/rules/checklist-pre-production-observability.md +99 -0
- package/templates/rules/checklist-pre-production-performance.md +109 -0
- package/templates/rules/checklist-pre-production-resilience.md +99 -0
- package/templates/rules/checklist-pre-production-security.md +86 -0
- package/templates/rules/config-secrets-boundary.md +57 -0
- package/templates/rules/config-typing-zod.md +78 -0
- package/templates/rules/contracts-governance.md +64 -0
- package/templates/rules/data-memory-consolidation.md +81 -0
- package/templates/rules/data-memory-invalidation.md +90 -0
- package/templates/rules/data-memory-scoring.md +154 -0
- package/templates/rules/data-migrations-forward-only.md +55 -0
- package/templates/rules/data-orphan-cleanup.md +142 -0
- package/templates/rules/data-ttl-types.md +100 -0
- package/templates/rules/eval-loop.md +52 -0
- package/templates/rules/frontier-deterministic-boundary.md +91 -0
- package/templates/rules/hexa-adapter-pattern.md +85 -0
- package/templates/rules/hexa-architecture.md +84 -0
- package/templates/rules/hexa-move-deterministic-out.md +93 -0
- package/templates/rules/hexa-recommended-stack.md +33 -0
- package/templates/rules/hexa-typescript-native.md +80 -0
- package/templates/rules/observability-alert-configuration.md +160 -0
- package/templates/rules/observability-llm-metrics.md +150 -0
- package/templates/rules/observability-startup-provider-log.md +128 -0
- package/templates/rules/observability-structured-json-logs.md +138 -0
- package/templates/rules/patterns-memory-router-tiered.md +139 -0
- package/templates/rules/patterns-prompt-compiler.md +126 -0
- package/templates/rules/patterns-request-id-propagation.md +137 -0
- package/templates/rules/process-adr-and-journal.md +50 -0
- package/templates/rules/provider-llm-auto-detection.md +54 -0
- package/templates/rules/provider-no-crash-missing-env.md +67 -0
- package/templates/rules/quality-codebase-metrics.md +115 -0
- package/templates/rules/quality-zod-input-validation.md +131 -0
- package/templates/rules/resilience-fail-open.md +65 -0
- package/templates/rules/resilience-multi-provider-fallback.md +100 -0
- package/templates/rules/resilience-retry-backoff.md +115 -0
- package/templates/rules/resilience-retryable-errors.md +107 -0
- package/templates/rules/routing-confidence-upgrade.md +89 -0
- package/templates/rules/routing-model-cost-ratios.md +68 -0
- package/templates/rules/routing-smart-complexity.md +131 -0
- package/templates/rules/scaling-db-connection-pooling.md +121 -0
- package/templates/rules/scaling-distributed-rate-limiting.md +141 -0
- package/templates/rules/scaling-state-externalization.md +84 -0
- package/templates/rules/security-prompt-injection.md +63 -0
- package/templates/rules/state-event-sourcing.md +66 -0
- package/templates/rules/tools-registry-pattern.md +153 -0
- package/templates/rules/tools-scope-atomicity.md +103 -0
- package/templates/targets/AGENTS.md +32 -0
- package/templates/workflows/architect.md +50 -0
- package/templates/workflows/brownfield.md +52 -0
- package/templates/workflows/decision-loop.md +52 -0
- package/templates/workflows/floor.md +60 -0
- package/templates/workflows/frame.md +69 -0
- package/templates/workflows/govern.md +55 -0
- package/templates/workflows/handover.md +55 -0
- package/templates/workflows/iterate.md +52 -0
- package/templates/workflows/method.md +53 -0
- package/templates/workflows/review.md +59 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A Thin Model Abstraction You Own (Not a Heavy Chain Framework)
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Reduces token overhead, improves debuggability, removes volatile dependencies by keeping a light abstraction you control instead of a heavy framework
|
|
5
|
+
tags: [architecture, typescript, llm, frameworks]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## A Thin Model Abstraction You Own
|
|
9
|
+
|
|
10
|
+
Heavy chain or agent frameworks add overhead that is rarely justified. Keep a light abstraction you control over the model, and call the provider through a direct SDK behind your own port.
|
|
11
|
+
|
|
12
|
+
**Why avoid a heavy chain framework as the default:**
|
|
13
|
+
|
|
14
|
+
1. **Token overhead** - the framework's internal prompts add to your cost.
|
|
15
|
+
2. **Abstraction leaks** - you cannot optimise what you cannot see through.
|
|
16
|
+
3. **Volatile ecosystem** - frequent breaking changes in fast-moving framework code.
|
|
17
|
+
4. **Debug difficulty** - stack traces run through framework internals.
|
|
18
|
+
5. **Unnecessary complexity** - most applications do not need generic chains or agents.
|
|
19
|
+
|
|
20
|
+
**Incorrect (a heavy framework owns your control flow):**
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { ChatModel } from "@chain-framework/core";
|
|
24
|
+
import { HumanMessage, SystemMessage } from "@chain-framework/core/messages";
|
|
25
|
+
import { StringOutputParser } from "@chain-framework/core/output_parsers";
|
|
26
|
+
|
|
27
|
+
const model = new ChatModel({ modelName: "mid-tier-model" });
|
|
28
|
+
const chain = model.pipe(new StringOutputParser());
|
|
29
|
+
|
|
30
|
+
const response = await chain.invoke([
|
|
31
|
+
new SystemMessage(systemPrompt),
|
|
32
|
+
new HumanMessage(userMessage),
|
|
33
|
+
]);
|
|
34
|
+
// You no longer control the prompt, the caching, or the cost.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Correct (a direct SDK behind a thin port you own):**
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// The adapter imports one provider SDK; the core sees only the port.
|
|
41
|
+
import { ModelSDK } from "@vendor/model-sdk"; // any provider's official SDK
|
|
42
|
+
|
|
43
|
+
const client = new ModelSDK();
|
|
44
|
+
|
|
45
|
+
const response = await client.complete({
|
|
46
|
+
model: "mid-tier-model",
|
|
47
|
+
maxTokens: 4096,
|
|
48
|
+
system: [corePrompt, toolsPrompt, dynamicContext],
|
|
49
|
+
messages: [{ role: "user", content: userMessage }],
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**When a framework might still help:**
|
|
54
|
+
|
|
55
|
+
- Rapid throwaway prototyping.
|
|
56
|
+
- Genuinely complex multi-agent orchestration that you have measured the need for.
|
|
57
|
+
|
|
58
|
+
**The abstraction you keep is yours:**
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// A thin interface you control, in the core. One adapter per provider implements it.
|
|
62
|
+
interface ModelPort {
|
|
63
|
+
complete(request: CompletionRequest): Promise<CompletionResponse>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class PrimaryModelAdapter implements ModelPort {
|
|
67
|
+
// full control over caching, retries, logging, cost attribution
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class SecondaryModelAdapter implements ModelPort {
|
|
71
|
+
// same interface, a different provider, swapped by configuration
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Benefits of the thin, owned abstraction:**
|
|
76
|
+
|
|
77
|
+
- Full control of caching and cost attribution.
|
|
78
|
+
- Simple debugging (no framework internals in the trace).
|
|
79
|
+
- No framework learning curve or breaking-change treadmill.
|
|
80
|
+
- The type safety and the contract are yours to define.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Alert Configuration
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Ensures timely notification of production issues
|
|
5
|
+
tags: [observability, alerting, production]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Alert Configuration
|
|
9
|
+
|
|
10
|
+
Configure alerts to catch issues before they impact users.
|
|
11
|
+
|
|
12
|
+
**Alert Tiers:**
|
|
13
|
+
|
|
14
|
+
| Tier | Response Time | Examples |
|
|
15
|
+
|------|---------------|----------|
|
|
16
|
+
| P0 - Critical | <15 min | Service down, data loss |
|
|
17
|
+
| P1 - High | <1 hour | Error rate spike, degraded performance |
|
|
18
|
+
| P2 - Medium | <4 hours | Elevated latency, cache degradation |
|
|
19
|
+
| P3 - Low | Next business day | Cost anomaly, capacity warning |
|
|
20
|
+
|
|
21
|
+
**Recommended Alerts:**
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
# alerts.yaml
|
|
25
|
+
alerts:
|
|
26
|
+
# P0 - Critical
|
|
27
|
+
- name: service_down
|
|
28
|
+
condition: healthcheck_failures > 3
|
|
29
|
+
window: 5m
|
|
30
|
+
severity: critical
|
|
31
|
+
notify: [pagerduty, slack-critical]
|
|
32
|
+
|
|
33
|
+
- name: error_rate_critical
|
|
34
|
+
condition: error_rate > 10%
|
|
35
|
+
window: 5m
|
|
36
|
+
severity: critical
|
|
37
|
+
notify: [pagerduty, slack-critical]
|
|
38
|
+
|
|
39
|
+
# P1 - High
|
|
40
|
+
- name: error_rate_elevated
|
|
41
|
+
condition: error_rate > 5%
|
|
42
|
+
window: 5m
|
|
43
|
+
severity: high
|
|
44
|
+
notify: [slack-alerts]
|
|
45
|
+
|
|
46
|
+
- name: llm_provider_failing
|
|
47
|
+
condition: llm_error_rate > 5%
|
|
48
|
+
window: 5m
|
|
49
|
+
severity: high
|
|
50
|
+
notify: [slack-alerts]
|
|
51
|
+
|
|
52
|
+
- name: latency_degraded
|
|
53
|
+
condition: p95_latency > 5000ms
|
|
54
|
+
window: 10m
|
|
55
|
+
severity: high
|
|
56
|
+
notify: [slack-alerts]
|
|
57
|
+
|
|
58
|
+
# P2 - Medium
|
|
59
|
+
- name: cache_hit_low
|
|
60
|
+
condition: cache_hit_rate < 50%
|
|
61
|
+
window: 30m
|
|
62
|
+
severity: medium
|
|
63
|
+
notify: [slack-engineering]
|
|
64
|
+
|
|
65
|
+
- name: db_connections_high
|
|
66
|
+
condition: db_pool_usage > 80%
|
|
67
|
+
window: 15m
|
|
68
|
+
severity: medium
|
|
69
|
+
notify: [slack-engineering]
|
|
70
|
+
|
|
71
|
+
# P3 - Low
|
|
72
|
+
- name: daily_cost_anomaly
|
|
73
|
+
condition: daily_cost > 2x avg_daily_cost
|
|
74
|
+
window: 24h
|
|
75
|
+
severity: low
|
|
76
|
+
notify: [email-team]
|
|
77
|
+
|
|
78
|
+
- name: disk_space_warning
|
|
79
|
+
condition: disk_usage > 70%
|
|
80
|
+
window: 1h
|
|
81
|
+
severity: low
|
|
82
|
+
notify: [slack-engineering]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Implementation with the telemetry sink:**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Alert configuration in code
|
|
89
|
+
const alertRules = [
|
|
90
|
+
{
|
|
91
|
+
name: 'High Error Rate',
|
|
92
|
+
query: `
|
|
93
|
+
SELECT count(*) as errors
|
|
94
|
+
FROM logs
|
|
95
|
+
WHERE level = 'error'
|
|
96
|
+
AND timestamp > now() - 5m
|
|
97
|
+
`,
|
|
98
|
+
threshold: { errors: 100 },
|
|
99
|
+
severity: 'high',
|
|
100
|
+
channels: ['slack-alerts'],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'LLM Cost Spike',
|
|
104
|
+
query: `
|
|
105
|
+
SELECT sum(estimatedCost) as cost
|
|
106
|
+
FROM llm_metrics
|
|
107
|
+
WHERE timestamp > now() - 1h
|
|
108
|
+
`,
|
|
109
|
+
threshold: { cost: 50 },
|
|
110
|
+
severity: 'medium',
|
|
111
|
+
channels: ['slack-engineering'],
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
// Alert evaluation
|
|
116
|
+
async function evaluateAlerts() {
|
|
117
|
+
for (const rule of alertRules) {
|
|
118
|
+
const result = await telemetry.query(rule.query);
|
|
119
|
+
|
|
120
|
+
if (exceedsThreshold(result, rule.threshold)) {
|
|
121
|
+
await sendAlert({
|
|
122
|
+
name: rule.name,
|
|
123
|
+
severity: rule.severity,
|
|
124
|
+
channels: rule.channels,
|
|
125
|
+
data: result,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Run every minute
|
|
132
|
+
cron.schedule('* * * * *', evaluateAlerts);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Alert Message Format:**
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface AlertMessage {
|
|
139
|
+
title: string;
|
|
140
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
141
|
+
summary: string;
|
|
142
|
+
metrics: Record<string, number>;
|
|
143
|
+
runbook?: string;
|
|
144
|
+
dashboardUrl?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Example Slack message
|
|
148
|
+
const message = {
|
|
149
|
+
title: 'High Error Rate Detected',
|
|
150
|
+
severity: 'high',
|
|
151
|
+
summary: 'Error rate exceeded 5% threshold for 5 minutes',
|
|
152
|
+
metrics: {
|
|
153
|
+
errorRate: 7.2,
|
|
154
|
+
errorCount: 156,
|
|
155
|
+
affectedEndpoints: 3,
|
|
156
|
+
},
|
|
157
|
+
runbook: 'https://wiki/runbooks/high-error-rate',
|
|
158
|
+
dashboardUrl: 'https://grafana/d/errors',
|
|
159
|
+
};
|
|
160
|
+
```
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: LLM Metrics Tracking
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Enables cost monitoring, performance optimization, and anomaly detection
|
|
5
|
+
tags: [observability, llm, metrics, cost]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## LLM Metrics Tracking
|
|
9
|
+
|
|
10
|
+
Track comprehensive metrics for every LLM call to enable cost control and optimization.
|
|
11
|
+
|
|
12
|
+
**Essential Metrics:**
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
interface LLMMetrics {
|
|
16
|
+
// Request metadata
|
|
17
|
+
requestId: string;
|
|
18
|
+
userId: string;
|
|
19
|
+
model: string;
|
|
20
|
+
provider: string;
|
|
21
|
+
|
|
22
|
+
// Token usage
|
|
23
|
+
inputTokens: number;
|
|
24
|
+
outputTokens: number;
|
|
25
|
+
cacheReadTokens: number;
|
|
26
|
+
cacheWriteTokens: number;
|
|
27
|
+
|
|
28
|
+
// Performance
|
|
29
|
+
latencyMs: number;
|
|
30
|
+
timeToFirstToken?: number;
|
|
31
|
+
|
|
32
|
+
// Cost
|
|
33
|
+
estimatedCost: number;
|
|
34
|
+
|
|
35
|
+
// Context
|
|
36
|
+
complexity: string;
|
|
37
|
+
cached: boolean;
|
|
38
|
+
retries: number;
|
|
39
|
+
|
|
40
|
+
// Timestamp
|
|
41
|
+
timestamp: Date;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Implementation:**
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// LLM wrapper with metrics
|
|
49
|
+
async function completeLLM(request: LLMRequest): Promise<LLMResponse> {
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
const metrics: Partial<LLMMetrics> = {
|
|
52
|
+
requestId: request.requestId,
|
|
53
|
+
userId: request.userId,
|
|
54
|
+
model: request.model,
|
|
55
|
+
provider: llmConfig.provider,
|
|
56
|
+
complexity: request.complexity,
|
|
57
|
+
timestamp: new Date(),
|
|
58
|
+
retries: 0,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const response = await withLLMRetry(
|
|
63
|
+
() => llmClient.complete(request),
|
|
64
|
+
{ onRetry: () => metrics.retries!++ }
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Extract usage
|
|
68
|
+
metrics.inputTokens = response.usage.input_tokens;
|
|
69
|
+
metrics.outputTokens = response.usage.output_tokens;
|
|
70
|
+
metrics.cacheReadTokens = response.usage.cache_read_input_tokens || 0;
|
|
71
|
+
metrics.cacheWriteTokens = response.usage.cache_creation_input_tokens || 0;
|
|
72
|
+
metrics.latencyMs = Date.now() - start;
|
|
73
|
+
metrics.cached = metrics.cacheReadTokens > 0;
|
|
74
|
+
metrics.estimatedCost = calculateCost(metrics as LLMMetrics);
|
|
75
|
+
|
|
76
|
+
// Log metrics
|
|
77
|
+
logger.info('LLM request completed', metrics);
|
|
78
|
+
|
|
79
|
+
// Send to metrics service
|
|
80
|
+
telemetry.record('llm_request', metrics);
|
|
81
|
+
|
|
82
|
+
return response;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
logger.error('LLM request failed', {
|
|
85
|
+
...metrics,
|
|
86
|
+
error: error.message,
|
|
87
|
+
latencyMs: Date.now() - start,
|
|
88
|
+
});
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Cost calculation.
|
|
94
|
+
// Typical cache pricing: reads at ~10% of the input price, writes at
|
|
95
|
+
// ~125% (verify against your provider's price sheet).
|
|
96
|
+
// CAUTION: usage field semantics vary by provider — some exclude cache
|
|
97
|
+
// read/write tokens from input_tokens, others include them. This formula
|
|
98
|
+
// assumes input_tokens EXCLUDES cache tokens; check your provider's
|
|
99
|
+
// usage schema before trusting the numbers, or you will double-count.
|
|
100
|
+
function calculateCost(metrics: LLMMetrics): number {
|
|
101
|
+
const pricing = MODEL_PRICING[metrics.model];
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
(metrics.inputTokens * pricing.input +
|
|
105
|
+
metrics.cacheReadTokens * pricing.input * 0.1 +
|
|
106
|
+
metrics.cacheWriteTokens * pricing.input * 1.25 +
|
|
107
|
+
metrics.outputTokens * pricing.output) / 1_000_000
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Aggregated Metrics:**
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Daily cost aggregation
|
|
116
|
+
interface DailyMetrics {
|
|
117
|
+
date: string;
|
|
118
|
+
totalRequests: number;
|
|
119
|
+
totalCost: number;
|
|
120
|
+
avgLatency: number;
|
|
121
|
+
cacheHitRate: number;
|
|
122
|
+
modelBreakdown: Record<string, { requests: number; cost: number }>;
|
|
123
|
+
errorRate: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function aggregateDailyMetrics(date: string): Promise<DailyMetrics> {
|
|
127
|
+
const metrics = await db.llmMetrics.findMany({
|
|
128
|
+
where: { date: startOfDay(date) },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
date,
|
|
133
|
+
totalRequests: metrics.length,
|
|
134
|
+
totalCost: sum(metrics.map(m => m.estimatedCost)),
|
|
135
|
+
avgLatency: avg(metrics.map(m => m.latencyMs)),
|
|
136
|
+
cacheHitRate: metrics.filter(m => m.cached).length / metrics.length,
|
|
137
|
+
modelBreakdown: groupByModel(metrics),
|
|
138
|
+
errorRate: metrics.filter(m => m.error).length / metrics.length,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Alert Thresholds:**
|
|
144
|
+
|
|
145
|
+
| Metric | Warning | Critical |
|
|
146
|
+
|--------|---------|----------|
|
|
147
|
+
| Cost/day | >$50 | >$100 |
|
|
148
|
+
| Error rate | >2% | >5% |
|
|
149
|
+
| P95 latency | >5s | >10s |
|
|
150
|
+
| Cache hit rate | <50% | <30% |
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Log Providers at Startup
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: Simplifies debugging by showing configuration at startup
|
|
5
|
+
tags: [observability, configuration, debugging]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Log Providers at Startup
|
|
9
|
+
|
|
10
|
+
Always log the detected configuration at application startup for debugging.
|
|
11
|
+
|
|
12
|
+
**Implementation:**
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// src/lib/startup.ts
|
|
16
|
+
import { features } from './features';
|
|
17
|
+
import { llmConfig } from './providers/llm-provider';
|
|
18
|
+
import { embeddingConfig } from './providers/embedding-provider';
|
|
19
|
+
|
|
20
|
+
export function logStartupConfig() {
|
|
21
|
+
console.info('═'.repeat(60));
|
|
22
|
+
console.info('[STARTUP] Application Configuration');
|
|
23
|
+
console.info('═'.repeat(60));
|
|
24
|
+
|
|
25
|
+
console.info('[CONFIG] Deployment mode:', features.mode);
|
|
26
|
+
|
|
27
|
+
console.info('[CONFIG] Providers:', {
|
|
28
|
+
llm: {
|
|
29
|
+
provider: llmConfig.provider,
|
|
30
|
+
model: llmConfig.model,
|
|
31
|
+
// Don't log API keys
|
|
32
|
+
},
|
|
33
|
+
embeddings: {
|
|
34
|
+
provider: embeddingConfig.provider,
|
|
35
|
+
model: embeddingConfig.model,
|
|
36
|
+
dimensions: embeddingConfig.dimensions,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.info('[CONFIG] Features:', {
|
|
41
|
+
auth: features.auth.provider,
|
|
42
|
+
search: features.search.enabled ? features.search.provider : 'disabled',
|
|
43
|
+
memory: features.memory.enabled ? features.memory.provider : 'disabled',
|
|
44
|
+
realtime: features.realtime.enabled,
|
|
45
|
+
presence: features.realtime.presence,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.info('[CONFIG] Database:', {
|
|
49
|
+
postgres: !!process.env.DATABASE_URL,
|
|
50
|
+
graph: !!process.env.GRAPH_URI,
|
|
51
|
+
redis: !!process.env.REDIS_URL,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.info('═'.repeat(60));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Call at startup
|
|
58
|
+
// app.ts or index.ts
|
|
59
|
+
logStartupConfig();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Output Example:**
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
════════════════════════════════════════════════════════════
|
|
66
|
+
[STARTUP] Application Configuration
|
|
67
|
+
════════════════════════════════════════════════════════════
|
|
68
|
+
[CONFIG] Deployment mode: full
|
|
69
|
+
[CONFIG] Providers: {
|
|
70
|
+
llm: { provider: 'configured-provider', model: 'mid-tier-model' },
|
|
71
|
+
embeddings: { provider: 'configured-provider', model: 'embedding-model', dimensions: 1536 }
|
|
72
|
+
}
|
|
73
|
+
[CONFIG] Features: {
|
|
74
|
+
auth: 'configured-provider',
|
|
75
|
+
search: 'web-search',
|
|
76
|
+
memory: 'configured-provider',
|
|
77
|
+
realtime: true,
|
|
78
|
+
realtime: true
|
|
79
|
+
}
|
|
80
|
+
[CONFIG] Database: { postgres: true, graph: true, redis: true }
|
|
81
|
+
════════════════════════════════════════════════════════════
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Environment Validation:**
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
function validateEnvironment() {
|
|
88
|
+
const errors: string[] = [];
|
|
89
|
+
const warnings: string[] = [];
|
|
90
|
+
|
|
91
|
+
// Required
|
|
92
|
+
if (!process.env.DATABASE_URL) {
|
|
93
|
+
errors.push('DATABASE_URL is required');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// At least one LLM provider
|
|
97
|
+
const hasLLM = [
|
|
98
|
+
'MODEL_GATEWAY_URL',
|
|
99
|
+
'MODEL_PROVIDER',
|
|
100
|
+
'AWS_ACCESS_KEY_ID',
|
|
101
|
+
'MODEL_API_KEY',
|
|
102
|
+
'EMBEDDING_API_KEY',
|
|
103
|
+
].some(key => !!process.env[key]);
|
|
104
|
+
|
|
105
|
+
if (!hasLLM) {
|
|
106
|
+
errors.push('No LLM provider configured');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Warnings for missing optional services
|
|
110
|
+
if (!process.env.REDIS_URL) {
|
|
111
|
+
warnings.push('REDIS_URL not set - using in-memory state (not scalable)');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Log results
|
|
115
|
+
if (warnings.length > 0) {
|
|
116
|
+
console.warn('[STARTUP] Warnings:', warnings);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (errors.length > 0) {
|
|
120
|
+
console.error('[STARTUP] Configuration errors:', errors);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Run before startup
|
|
126
|
+
validateEnvironment();
|
|
127
|
+
logStartupConfig();
|
|
128
|
+
```
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Structured JSON Logs
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Enables log querying, aggregation, and alerting in production
|
|
5
|
+
tags: [observability, logging, production]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Structured JSON Logs
|
|
9
|
+
|
|
10
|
+
Use structured JSON logs for queryable, parseable logging in production.
|
|
11
|
+
|
|
12
|
+
**Incorrect:**
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// Unstructured logs - hard to parse and query
|
|
16
|
+
console.log(`User ${userId} created project ${projectId} in ${duration}ms`);
|
|
17
|
+
console.log('Error:', error.message);
|
|
18
|
+
console.log(`Request from ${req.ip} to ${req.path}`);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Correct:**
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// Structured logger
|
|
25
|
+
interface LogEntry {
|
|
26
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
27
|
+
message: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
requestId?: string;
|
|
30
|
+
userId?: string;
|
|
31
|
+
tenantId?: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class StructuredLogger {
|
|
36
|
+
private context: Partial<LogEntry> = {};
|
|
37
|
+
|
|
38
|
+
child(context: Partial<LogEntry>): StructuredLogger {
|
|
39
|
+
const child = new StructuredLogger();
|
|
40
|
+
child.context = { ...this.context, ...context };
|
|
41
|
+
return child;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private log(level: LogEntry['level'], message: string, data: object = {}) {
|
|
45
|
+
const entry: LogEntry = {
|
|
46
|
+
level,
|
|
47
|
+
message,
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
...this.context,
|
|
50
|
+
...data,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Redact sensitive fields
|
|
54
|
+
const redacted = this.redactSensitive(entry);
|
|
55
|
+
|
|
56
|
+
console.log(JSON.stringify(redacted));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private redactSensitive(entry: LogEntry): LogEntry {
|
|
60
|
+
const sensitiveKeys = ['password', 'token', 'apiKey', 'secret', 'authorization'];
|
|
61
|
+
const redacted = { ...entry };
|
|
62
|
+
|
|
63
|
+
for (const key of Object.keys(redacted)) {
|
|
64
|
+
if (sensitiveKeys.some(s => key.toLowerCase().includes(s))) {
|
|
65
|
+
redacted[key] = '[REDACTED]';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return redacted;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
debug(message: string, data?: object) { this.log('debug', message, data); }
|
|
73
|
+
info(message: string, data?: object) { this.log('info', message, data); }
|
|
74
|
+
warn(message: string, data?: object) { this.log('warn', message, data); }
|
|
75
|
+
error(message: string, data?: object) { this.log('error', message, data); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const logger = new StructuredLogger();
|
|
79
|
+
|
|
80
|
+
// Usage
|
|
81
|
+
logger.info('Project created', {
|
|
82
|
+
userId: 'usr_123',
|
|
83
|
+
projectId: 'prj_456',
|
|
84
|
+
duration: 45,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Output:
|
|
88
|
+
// {"level":"info","message":"Project created","timestamp":"2025-01-24T10:30:00.000Z","userId":"usr_123","projectId":"prj_456","duration":45}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Request Context Logger:**
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Middleware to add request context
|
|
95
|
+
function requestLoggerMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
96
|
+
const requestId = req.headers['x-request-id'] as string || crypto.randomUUID();
|
|
97
|
+
const requestLogger = logger.child({
|
|
98
|
+
requestId,
|
|
99
|
+
method: req.method,
|
|
100
|
+
path: req.path,
|
|
101
|
+
userId: req.user?.id,
|
|
102
|
+
tenantId: req.user?.tenantId,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
req.logger = requestLogger;
|
|
106
|
+
|
|
107
|
+
// Log request start
|
|
108
|
+
requestLogger.info('Request started');
|
|
109
|
+
|
|
110
|
+
// Log request end
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
res.on('finish', () => {
|
|
113
|
+
requestLogger.info('Request completed', {
|
|
114
|
+
status: res.statusCode,
|
|
115
|
+
duration: Date.now() - start,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
next();
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Log Query Examples (the telemetry sink/Datadog):**
|
|
124
|
+
|
|
125
|
+
```sql
|
|
126
|
+
-- Find slow LLM requests
|
|
127
|
+
SELECT * FROM logs
|
|
128
|
+
WHERE message = 'LLM request completed'
|
|
129
|
+
AND duration > 5000
|
|
130
|
+
ORDER BY timestamp DESC
|
|
131
|
+
|
|
132
|
+
-- Error rate by endpoint
|
|
133
|
+
SELECT path, COUNT(*) as errors
|
|
134
|
+
FROM logs
|
|
135
|
+
WHERE level = 'error'
|
|
136
|
+
GROUP BY path
|
|
137
|
+
ORDER BY errors DESC
|
|
138
|
+
```
|