ecip-observability-stack 1.0.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.md +48 -0
- package/README.md +75 -0
- package/alerts/analysis-backlog.yaml +39 -0
- package/alerts/cache-degradation.yaml +44 -0
- package/alerts/dlq-depth.yaml +56 -0
- package/alerts/lsp-daemon.yaml +43 -0
- package/alerts/mcp-latency.yaml +46 -0
- package/alerts/security-anomaly.yaml +59 -0
- package/alerts/sla-latency.yaml +61 -0
- package/chaos/kafka-broker-restart.sh +168 -0
- package/chaos/kill-lsp-daemon.sh +148 -0
- package/chaos/redis-node-failure.sh +318 -0
- package/ci/check-observability-contract.js +285 -0
- package/ci/eslint-plugin-ecip/index.js +209 -0
- package/ci/eslint-plugin-ecip/package.json +12 -0
- package/ci/github-actions-observability-gate.yaml +180 -0
- package/ci/ruff-shared.toml +41 -0
- package/collector/otel-collector-config.yaml +226 -0
- package/collector/otel-collector-daemonset.yaml +168 -0
- package/collector/sampling-config.yaml +83 -0
- package/dashboards/_provisioning/grafana-dashboards.yaml +16 -0
- package/dashboards/analysis-throughput.json +166 -0
- package/dashboards/cache-performance.json +129 -0
- package/dashboards/cross-repo-fanout.json +93 -0
- package/dashboards/event-bus-dlq.json +129 -0
- package/dashboards/lsp-daemon-health.json +104 -0
- package/dashboards/mcp-call-graph.json +114 -0
- package/dashboards/query-latency.json +160 -0
- package/dashboards/security-events.json +131 -0
- package/docs/M08-Observability-Design.md +639 -0
- package/docs/PROGRESS.md +375 -0
- package/docs/module-documentation.md +64 -0
- package/elasticsearch/ilm-policy.json +57 -0
- package/elasticsearch/index-template.json +62 -0
- package/elasticsearch/kibana-space.yaml +53 -0
- package/helm/Chart.yaml +30 -0
- package/helm/templates/configmaps.yaml +25 -0
- package/helm/templates/elasticsearch.yaml +68 -0
- package/helm/templates/grafana-secret.yaml +22 -0
- package/helm/templates/grafana.yaml +19 -0
- package/helm/templates/loki.yaml +33 -0
- package/helm/templates/otel-collector.yaml +119 -0
- package/helm/templates/prometheus.yaml +43 -0
- package/helm/templates/tempo.yaml +16 -0
- package/helm/values.prod.yaml +159 -0
- package/helm/values.yaml +146 -0
- package/logging-lib/nodejs/package.json +57 -0
- package/logging-lib/nodejs/pnpm-lock.yaml +4576 -0
- package/logging-lib/python/pyproject.toml +45 -0
- package/logging-lib/python/src/__init__.py +19 -0
- package/logging-lib/python/src/logger.py +131 -0
- package/logging-lib/python/src/security_events.py +150 -0
- package/logging-lib/python/src/tracer.py +185 -0
- package/logging-lib/python/tests/test_logger.py +113 -0
- package/package.json +21 -0
- package/prometheus/prometheus-values.yaml +170 -0
- package/prometheus/recording-rules.yaml +97 -0
- package/prometheus/scrape-configs.yaml +122 -0
- package/runbooks/SDK-INTEGRATION.md +239 -0
- package/runbooks/alert-response/ANALYSIS_BACKLOG.md +128 -0
- package/runbooks/alert-response/DLQ_DEPTH_EXCEEDED.md +150 -0
- package/runbooks/alert-response/HIGH_QUERY_LATENCY.md +134 -0
- package/runbooks/alert-response/LSP_DAEMON_RESTART.md +118 -0
- package/runbooks/alert-response/SECURITY_ANOMALY.md +160 -0
- package/runbooks/dashboard-guide.md +169 -0
- package/scripts/lint-dashboards.js +184 -0
- package/tempo/tempo-datasource.yaml +46 -0
- package/tempo/tempo-values.yaml +94 -0
- package/tests/alert-threshold-config.test.ts +283 -0
- package/tests/log-schema-validation.test.ts +246 -0
- package/tests/metric-label-validation.test.ts +292 -0
- package/tests/otel-pipeline-integration.test.ts +420 -0
- package/tests/security-events.test.ts +417 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +21 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ECIP M08 — CI Gate: Observability Contract Checker
|
|
4
|
+
*
|
|
5
|
+
* Validates that a module satisfies the M08 instrumentation contract (§5).
|
|
6
|
+
* Run against any module directory to verify compliance.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node ci/check-observability-contract.js <module-dir>
|
|
10
|
+
* node ci/check-observability-contract.js ../ecip-api-gateway
|
|
11
|
+
*
|
|
12
|
+
* Checks performed:
|
|
13
|
+
* 1. @ecip/observability (or ecip-observability for Python) is in dependencies
|
|
14
|
+
* 2. initTracer() is called in an entry-point file (before server start)
|
|
15
|
+
* 3. createLogger() / get_logger() is used in at least one handler
|
|
16
|
+
* 4. No console.log / print() in production source files
|
|
17
|
+
*
|
|
18
|
+
* Exit codes:
|
|
19
|
+
* 0 — all checks pass
|
|
20
|
+
* 1 — one or more checks failed
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Configuration
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const NODEJS_SDK = '@ecip/observability';
|
|
33
|
+
const PYTHON_SDK = 'ecip-observability';
|
|
34
|
+
|
|
35
|
+
const ENTRY_PATTERNS = [
|
|
36
|
+
/server\.[tj]sx?$/,
|
|
37
|
+
/app\.[tj]sx?$/,
|
|
38
|
+
/main\.[tj]sx?$/,
|
|
39
|
+
/instrument\.[tj]sx?$/,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const EXCLUDED_DIRS = new Set([
|
|
43
|
+
'node_modules', '.git', 'dist', 'build', 'coverage',
|
|
44
|
+
'__pycache__', '.venv', 'venv', '.mypy_cache',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const EXCLUDED_FILE_PATTERNS = [
|
|
48
|
+
/\.test\.[tj]sx?$/,
|
|
49
|
+
/\.spec\.[tj]sx?$/,
|
|
50
|
+
/test_.*\.py$/,
|
|
51
|
+
/conftest\.py$/,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Helpers
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function walkDir(dir, ext) {
|
|
59
|
+
const results = [];
|
|
60
|
+
if (!fs.existsSync(dir)) return results;
|
|
61
|
+
|
|
62
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
63
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
64
|
+
const full = path.join(dir, entry.name);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
results.push(...walkDir(full, ext));
|
|
67
|
+
} else if (ext.some((e) => entry.name.endsWith(e))) {
|
|
68
|
+
results.push(full);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isTestFile(filePath) {
|
|
75
|
+
const rel = path.basename(filePath);
|
|
76
|
+
return EXCLUDED_FILE_PATTERNS.some((p) => p.test(rel));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Checks
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
function checkNodeDependency(moduleDir) {
|
|
84
|
+
const pkgPath = path.join(moduleDir, 'package.json');
|
|
85
|
+
if (!fs.existsSync(pkgPath)) return { skip: true };
|
|
86
|
+
|
|
87
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
88
|
+
const allDeps = {
|
|
89
|
+
...pkg.dependencies,
|
|
90
|
+
...pkg.devDependencies,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (allDeps[NODEJS_SDK]) {
|
|
94
|
+
return { pass: true, detail: `${NODEJS_SDK} found in package.json` };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
pass: false,
|
|
98
|
+
detail: `${NODEJS_SDK} is NOT in package.json dependencies. Run: npm install ${NODEJS_SDK}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function checkPythonDependency(moduleDir) {
|
|
103
|
+
const pyprojectPath = path.join(moduleDir, 'pyproject.toml');
|
|
104
|
+
const requirementsPath = path.join(moduleDir, 'requirements.txt');
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(pyprojectPath) && !fs.existsSync(requirementsPath)) {
|
|
107
|
+
return { skip: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
111
|
+
const content = fs.readFileSync(pyprojectPath, 'utf8');
|
|
112
|
+
if (content.includes(PYTHON_SDK)) {
|
|
113
|
+
return { pass: true, detail: `${PYTHON_SDK} found in pyproject.toml` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (fs.existsSync(requirementsPath)) {
|
|
118
|
+
const content = fs.readFileSync(requirementsPath, 'utf8');
|
|
119
|
+
if (content.includes(PYTHON_SDK)) {
|
|
120
|
+
return { pass: true, detail: `${PYTHON_SDK} found in requirements.txt` };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
pass: false,
|
|
126
|
+
detail: `${PYTHON_SDK} is NOT in pyproject.toml or requirements.txt. Run: pip install ${PYTHON_SDK}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function checkInitTracer(moduleDir) {
|
|
131
|
+
// Check Node.js files
|
|
132
|
+
const tsFiles = walkDir(path.join(moduleDir, 'src'), ['.ts', '.tsx', '.js', '.jsx']);
|
|
133
|
+
for (const f of tsFiles) {
|
|
134
|
+
const content = fs.readFileSync(f, 'utf8');
|
|
135
|
+
if (content.includes('initTracer(') || content.includes('initTracer (')) {
|
|
136
|
+
return { pass: true, detail: `initTracer() found in ${path.relative(moduleDir, f)}` };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check Python files
|
|
141
|
+
const pyFiles = walkDir(path.join(moduleDir, 'src'), ['.py']);
|
|
142
|
+
for (const f of pyFiles) {
|
|
143
|
+
const content = fs.readFileSync(f, 'utf8');
|
|
144
|
+
if (content.includes('init_tracer(') || content.includes('init_tracer (')) {
|
|
145
|
+
return { pass: true, detail: `init_tracer() found in ${path.relative(moduleDir, f)}` };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
pass: false,
|
|
151
|
+
detail: 'initTracer() / init_tracer() not found in any source file under src/. See runbooks/SDK-INTEGRATION.md.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function checkLoggerUsage(moduleDir) {
|
|
156
|
+
const tsFiles = walkDir(path.join(moduleDir, 'src'), ['.ts', '.tsx', '.js', '.jsx']);
|
|
157
|
+
for (const f of tsFiles) {
|
|
158
|
+
if (isTestFile(f)) continue;
|
|
159
|
+
const content = fs.readFileSync(f, 'utf8');
|
|
160
|
+
if (content.includes('createLogger(') || content.includes('createLogger (')) {
|
|
161
|
+
return { pass: true, detail: `createLogger() found in ${path.relative(moduleDir, f)}` };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const pyFiles = walkDir(path.join(moduleDir, 'src'), ['.py']);
|
|
166
|
+
for (const f of pyFiles) {
|
|
167
|
+
if (isTestFile(f)) continue;
|
|
168
|
+
const content = fs.readFileSync(f, 'utf8');
|
|
169
|
+
if (content.includes('get_logger(') || content.includes('get_logger (')) {
|
|
170
|
+
return { pass: true, detail: `get_logger() found in ${path.relative(moduleDir, f)}` };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
pass: false,
|
|
176
|
+
detail: 'No createLogger() / get_logger() usage found in src/. At least one handler must use the structured logger.',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function checkNoConsoleLog(moduleDir) {
|
|
181
|
+
const violations = [];
|
|
182
|
+
|
|
183
|
+
// Node.js: console.log / console.warn / console.error
|
|
184
|
+
const tsFiles = walkDir(path.join(moduleDir, 'src'), ['.ts', '.tsx', '.js', '.jsx']);
|
|
185
|
+
for (const f of tsFiles) {
|
|
186
|
+
if (isTestFile(f)) continue;
|
|
187
|
+
const lines = fs.readFileSync(f, 'utf8').split('\n');
|
|
188
|
+
lines.forEach((line, i) => {
|
|
189
|
+
if (/console\.(log|warn|error|info|debug)\s*\(/.test(line)) {
|
|
190
|
+
violations.push(`${path.relative(moduleDir, f)}:${i + 1}: ${line.trim()}`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Python: print()
|
|
196
|
+
const pyFiles = walkDir(path.join(moduleDir, 'src'), ['.py']);
|
|
197
|
+
for (const f of pyFiles) {
|
|
198
|
+
if (isTestFile(f)) continue;
|
|
199
|
+
const lines = fs.readFileSync(f, 'utf8').split('\n');
|
|
200
|
+
lines.forEach((line, i) => {
|
|
201
|
+
// Match print( but not # print( in comments
|
|
202
|
+
if (/^\s*[^#]*\bprint\s*\(/.test(line)) {
|
|
203
|
+
violations.push(`${path.relative(moduleDir, f)}:${i + 1}: ${line.trim()}`);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (violations.length === 0) {
|
|
209
|
+
return { pass: true, detail: 'No console.log/print() found in production code' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
pass: false,
|
|
214
|
+
detail: `Found ${violations.length} console.log/print() violation(s):\n${violations.map((v) => ` ${v}`).join('\n')}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Main
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
function main() {
|
|
223
|
+
const moduleDir = process.argv[2];
|
|
224
|
+
if (!moduleDir) {
|
|
225
|
+
console.error('Usage: node check-observability-contract.js <module-directory>');
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const resolved = path.resolve(moduleDir);
|
|
230
|
+
if (!fs.existsSync(resolved)) {
|
|
231
|
+
console.error(`Directory not found: ${resolved}`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(`\n🔍 ECIP M08 — Observability Contract Check`);
|
|
236
|
+
console.log(` Module: ${path.basename(resolved)}`);
|
|
237
|
+
console.log(` Path: ${resolved}\n`);
|
|
238
|
+
|
|
239
|
+
// Detect language
|
|
240
|
+
const hasPackageJson = fs.existsSync(path.join(resolved, 'package.json'));
|
|
241
|
+
const hasPyproject = fs.existsSync(path.join(resolved, 'pyproject.toml'));
|
|
242
|
+
const hasRequirements = fs.existsSync(path.join(resolved, 'requirements.txt'));
|
|
243
|
+
const isPython = hasPyproject || hasRequirements;
|
|
244
|
+
const isNode = hasPackageJson;
|
|
245
|
+
|
|
246
|
+
if (!isNode && !isPython) {
|
|
247
|
+
console.log('⚠️ No package.json or pyproject.toml found — cannot determine language. Skipping.');
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const checks = [
|
|
252
|
+
{ name: 'SDK Dependency', fn: isNode ? () => checkNodeDependency(resolved) : () => checkPythonDependency(resolved) },
|
|
253
|
+
{ name: 'initTracer() Present', fn: () => checkInitTracer(resolved) },
|
|
254
|
+
{ name: 'Logger Usage', fn: () => checkLoggerUsage(resolved) },
|
|
255
|
+
{ name: 'No console.log/print()', fn: () => checkNoConsoleLog(resolved) },
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
let failures = 0;
|
|
259
|
+
for (const check of checks) {
|
|
260
|
+
const result = check.fn();
|
|
261
|
+
if (result.skip) {
|
|
262
|
+
console.log(` ⏭ ${check.name}: skipped (not applicable)`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (result.pass) {
|
|
266
|
+
console.log(` ✅ ${check.name}: ${result.detail}`);
|
|
267
|
+
} else {
|
|
268
|
+
console.log(` ❌ ${check.name}: FAILED`);
|
|
269
|
+
console.log(` ${result.detail}`);
|
|
270
|
+
failures++;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log('');
|
|
275
|
+
if (failures > 0) {
|
|
276
|
+
console.log(`❌ ${failures} check(s) failed. Module is NOT compliant with the M08 observability contract.`);
|
|
277
|
+
console.log(' See: ecip-observability-stack/runbooks/SDK-INTEGRATION.md\n');
|
|
278
|
+
process.exit(1);
|
|
279
|
+
} else {
|
|
280
|
+
console.log('✅ All checks passed. Module is compliant with the M08 observability contract.\n');
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
main();
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eslint-plugin-ecip — Shared ESLint rules for ECIP modules
|
|
3
|
+
*
|
|
4
|
+
* Enforces M08 observability contract:
|
|
5
|
+
* 1. No console.log/warn/error in production code (use @ecip/observability logger)
|
|
6
|
+
* 2. @ecip/observability must be imported in entry files
|
|
7
|
+
* 3. initTracer() must be called before server starts
|
|
8
|
+
*
|
|
9
|
+
* Usage in consuming module:
|
|
10
|
+
* npm install --save-dev eslint-plugin-ecip
|
|
11
|
+
* // .eslintrc.json
|
|
12
|
+
* { "plugins": ["ecip"], "extends": ["plugin:ecip/recommended"] }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
rules: {
|
|
19
|
+
/**
|
|
20
|
+
* Rule: no-console-log
|
|
21
|
+
*
|
|
22
|
+
* Bans console.log, console.warn, console.error, console.info in
|
|
23
|
+
* production source files. Test files and scripts are excluded via
|
|
24
|
+
* the recommended config's overrides.
|
|
25
|
+
*
|
|
26
|
+
* Why: console.* bypasses the structured logging pipeline. Logs
|
|
27
|
+
* emitted via console are unstructured plaintext without trace_id,
|
|
28
|
+
* module, repo, or branch context. They cannot be queried, alerted
|
|
29
|
+
* on, or correlated with distributed traces.
|
|
30
|
+
*/
|
|
31
|
+
'no-console-log': {
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'problem',
|
|
34
|
+
docs: {
|
|
35
|
+
description: 'Disallow console.log/warn/error in production code — use @ecip/observability createLogger() instead',
|
|
36
|
+
recommended: true,
|
|
37
|
+
},
|
|
38
|
+
messages: {
|
|
39
|
+
noConsole:
|
|
40
|
+
'Do not use console.{{ method }}() in production code. Use createLogger() from @ecip/observability instead. See runbooks/SDK-INTEGRATION.md.',
|
|
41
|
+
},
|
|
42
|
+
schema: [],
|
|
43
|
+
},
|
|
44
|
+
create(context) {
|
|
45
|
+
return {
|
|
46
|
+
CallExpression(node) {
|
|
47
|
+
if (
|
|
48
|
+
node.callee.type === 'MemberExpression' &&
|
|
49
|
+
node.callee.object.type === 'Identifier' &&
|
|
50
|
+
node.callee.object.name === 'console' &&
|
|
51
|
+
node.callee.property.type === 'Identifier' &&
|
|
52
|
+
['log', 'warn', 'error', 'info', 'debug', 'trace'].includes(
|
|
53
|
+
node.callee.property.name,
|
|
54
|
+
)
|
|
55
|
+
) {
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
messageId: 'noConsole',
|
|
59
|
+
data: { method: node.callee.property.name },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Rule: require-observability-import
|
|
69
|
+
*
|
|
70
|
+
* Ensures that files matching entry-point patterns (server.ts,
|
|
71
|
+
* app.ts, main.ts, index.ts) import from '@ecip/observability'.
|
|
72
|
+
*/
|
|
73
|
+
'require-observability-import': {
|
|
74
|
+
meta: {
|
|
75
|
+
type: 'problem',
|
|
76
|
+
docs: {
|
|
77
|
+
description: 'Entry-point files must import @ecip/observability',
|
|
78
|
+
recommended: true,
|
|
79
|
+
},
|
|
80
|
+
messages: {
|
|
81
|
+
missingImport:
|
|
82
|
+
'Entry-point file must import from \'@ecip/observability\'. See runbooks/SDK-INTEGRATION.md for setup instructions.',
|
|
83
|
+
},
|
|
84
|
+
schema: [],
|
|
85
|
+
},
|
|
86
|
+
create(context) {
|
|
87
|
+
const filename = context.getFilename();
|
|
88
|
+
const isEntryPoint = /(?:server|app|main|index)\.[tj]sx?$/.test(filename);
|
|
89
|
+
|
|
90
|
+
if (!isEntryPoint) return {};
|
|
91
|
+
|
|
92
|
+
let hasObservabilityImport = false;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
ImportDeclaration(node) {
|
|
96
|
+
if (
|
|
97
|
+
node.source.value === '@ecip/observability' ||
|
|
98
|
+
node.source.value.startsWith('@ecip/observability/')
|
|
99
|
+
) {
|
|
100
|
+
hasObservabilityImport = true;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
'Program:exit'(node) {
|
|
104
|
+
if (!hasObservabilityImport) {
|
|
105
|
+
context.report({
|
|
106
|
+
node,
|
|
107
|
+
messageId: 'missingImport',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Rule: require-init-tracer
|
|
117
|
+
*
|
|
118
|
+
* Entry point files that import @ecip/observability must call
|
|
119
|
+
* initTracer() before any server/app.listen/fastify.listen call.
|
|
120
|
+
*/
|
|
121
|
+
'require-init-tracer': {
|
|
122
|
+
meta: {
|
|
123
|
+
type: 'problem',
|
|
124
|
+
docs: {
|
|
125
|
+
description: 'initTracer() must be called in entry-point files before the server starts',
|
|
126
|
+
recommended: true,
|
|
127
|
+
},
|
|
128
|
+
messages: {
|
|
129
|
+
missingInitTracer:
|
|
130
|
+
'Entry-point imports @ecip/observability but does not call initTracer(). Tracer must be initialized before the server starts accepting requests.',
|
|
131
|
+
},
|
|
132
|
+
schema: [],
|
|
133
|
+
},
|
|
134
|
+
create(context) {
|
|
135
|
+
const filename = context.getFilename();
|
|
136
|
+
const isEntryPoint = /(?:server|app|main|index)\.[tj]sx?$/.test(filename);
|
|
137
|
+
|
|
138
|
+
if (!isEntryPoint) return {};
|
|
139
|
+
|
|
140
|
+
let hasObservabilityImport = false;
|
|
141
|
+
let hasInitTracer = false;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
ImportDeclaration(node) {
|
|
145
|
+
if (
|
|
146
|
+
node.source.value === '@ecip/observability' ||
|
|
147
|
+
node.source.value.startsWith('@ecip/observability/')
|
|
148
|
+
) {
|
|
149
|
+
hasObservabilityImport = true;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
CallExpression(node) {
|
|
153
|
+
if (
|
|
154
|
+
node.callee.type === 'Identifier' &&
|
|
155
|
+
node.callee.name === 'initTracer'
|
|
156
|
+
) {
|
|
157
|
+
hasInitTracer = true;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
'Program:exit'(node) {
|
|
161
|
+
if (hasObservabilityImport && !hasInitTracer) {
|
|
162
|
+
context.report({
|
|
163
|
+
node,
|
|
164
|
+
messageId: 'missingInitTracer',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
configs: {
|
|
174
|
+
recommended: {
|
|
175
|
+
plugins: ['ecip'],
|
|
176
|
+
rules: {
|
|
177
|
+
'ecip/no-console-log': 'error',
|
|
178
|
+
'ecip/require-observability-import': 'error',
|
|
179
|
+
'ecip/require-init-tracer': 'error',
|
|
180
|
+
},
|
|
181
|
+
overrides: [
|
|
182
|
+
{
|
|
183
|
+
// Exclude test files and scripts from console.log ban
|
|
184
|
+
files: [
|
|
185
|
+
'**/*.test.ts',
|
|
186
|
+
'**/*.test.js',
|
|
187
|
+
'**/*.spec.ts',
|
|
188
|
+
'**/*.spec.js',
|
|
189
|
+
'**/test/**',
|
|
190
|
+
'**/tests/**',
|
|
191
|
+
'**/scripts/**',
|
|
192
|
+
'**/__tests__/**',
|
|
193
|
+
],
|
|
194
|
+
rules: {
|
|
195
|
+
'ecip/no-console-log': 'off',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
// Only check init-tracer and observability-import in entry points
|
|
200
|
+
files: ['**/src/**'],
|
|
201
|
+
rules: {
|
|
202
|
+
'ecip/require-observability-import': 'error',
|
|
203
|
+
'ecip/require-init-tracer': 'error',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-ecip",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESLint rules enforcing ECIP M08 observability contract — console.log ban, initTracer() requirement",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": ["eslint", "eslintplugin", "ecip", "observability"],
|
|
7
|
+
"author": "ECIP Platform Team",
|
|
8
|
+
"license": "UNLICENSED",
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"eslint": ">=8.0.0"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# ECIP M08 — CI Gate: Observability Contract Enforcement
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Runs on every PR that modifies a module's source code.
|
|
5
|
+
# Enforces the M08 SDK integration contract (Design Doc §5).
|
|
6
|
+
#
|
|
7
|
+
# Checks:
|
|
8
|
+
# 1. ESLint ecip/no-console-log + require-observability-import + require-init-tracer
|
|
9
|
+
# 2. Ruff T20 (print() ban) for Python modules
|
|
10
|
+
# 3. Observability contract check (dependency, initTracer, logger, console.log)
|
|
11
|
+
# 4. promtool check rules (alert YAML validation)
|
|
12
|
+
# 5. Dashboard JSON lint
|
|
13
|
+
# =============================================================================
|
|
14
|
+
|
|
15
|
+
name: M08 Observability CI Gate
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
pull_request:
|
|
19
|
+
paths:
|
|
20
|
+
- 'ecip-api-gateway/src/**'
|
|
21
|
+
- 'ecip-analysis-engine/src/**'
|
|
22
|
+
- 'ecip-knowledge-store/src/**'
|
|
23
|
+
- 'ecip-query-service/src/**'
|
|
24
|
+
- 'ecip-mcp-server/src/**'
|
|
25
|
+
- 'ecip-registry-service/src/**'
|
|
26
|
+
- 'ecip-event-bus/src/**'
|
|
27
|
+
- 'ecip-observability-stack/alerts/**'
|
|
28
|
+
- 'ecip-observability-stack/dashboards/**'
|
|
29
|
+
- 'ecip-observability-stack/collector/**'
|
|
30
|
+
push:
|
|
31
|
+
branches: [main]
|
|
32
|
+
paths:
|
|
33
|
+
- 'ecip-observability-stack/alerts/**'
|
|
34
|
+
- 'ecip-observability-stack/dashboards/**'
|
|
35
|
+
|
|
36
|
+
jobs:
|
|
37
|
+
# -------------------------------------------------------------------------
|
|
38
|
+
# Job 1: Verify module observability contract (per-module)
|
|
39
|
+
# -------------------------------------------------------------------------
|
|
40
|
+
contract-check:
|
|
41
|
+
name: Observability Contract — ${{ matrix.module }}
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
strategy:
|
|
44
|
+
fail-fast: false
|
|
45
|
+
matrix:
|
|
46
|
+
module:
|
|
47
|
+
- ecip-api-gateway
|
|
48
|
+
- ecip-event-bus
|
|
49
|
+
- ecip-knowledge-store
|
|
50
|
+
- ecip-registry-service
|
|
51
|
+
# Uncomment as modules come online:
|
|
52
|
+
# - ecip-query-service # M04 — Week 11
|
|
53
|
+
# - ecip-mcp-server # M05 — Week 17
|
|
54
|
+
# - ecip-analysis-engine # M02 — Week 5 (Python)
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
|
|
58
|
+
- uses: actions/setup-node@v4
|
|
59
|
+
with:
|
|
60
|
+
node-version: '20'
|
|
61
|
+
|
|
62
|
+
- name: Check observability contract
|
|
63
|
+
run: node ecip-observability-stack/ci/check-observability-contract.js ${{ matrix.module }}
|
|
64
|
+
|
|
65
|
+
# -------------------------------------------------------------------------
|
|
66
|
+
# Job 2: ESLint with ecip plugin (Node.js modules)
|
|
67
|
+
# -------------------------------------------------------------------------
|
|
68
|
+
eslint-ecip:
|
|
69
|
+
name: ESLint ecip rules — ${{ matrix.module }}
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
strategy:
|
|
72
|
+
fail-fast: false
|
|
73
|
+
matrix:
|
|
74
|
+
module:
|
|
75
|
+
- ecip-api-gateway
|
|
76
|
+
- ecip-event-bus
|
|
77
|
+
- ecip-knowledge-store
|
|
78
|
+
- ecip-registry-service
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/checkout@v4
|
|
81
|
+
|
|
82
|
+
- uses: actions/setup-node@v4
|
|
83
|
+
with:
|
|
84
|
+
node-version: '20'
|
|
85
|
+
|
|
86
|
+
- name: Install module dependencies
|
|
87
|
+
working-directory: ${{ matrix.module }}
|
|
88
|
+
run: npm ci --ignore-scripts
|
|
89
|
+
|
|
90
|
+
- name: Install ESLint plugin
|
|
91
|
+
working-directory: ${{ matrix.module }}
|
|
92
|
+
run: npm install --no-save eslint ../ecip-observability-stack/ci/eslint-plugin-ecip
|
|
93
|
+
|
|
94
|
+
- name: Run ESLint with ecip rules
|
|
95
|
+
working-directory: ${{ matrix.module }}
|
|
96
|
+
run: npx eslint src/ --plugin ecip --rule 'ecip/no-console-log: error' --rule 'ecip/require-observability-import: error' --rule 'ecip/require-init-tracer: error'
|
|
97
|
+
|
|
98
|
+
# -------------------------------------------------------------------------
|
|
99
|
+
# Job 3: Ruff (Python modules)
|
|
100
|
+
# -------------------------------------------------------------------------
|
|
101
|
+
ruff-check:
|
|
102
|
+
name: Ruff — ecip-analysis-engine
|
|
103
|
+
runs-on: ubuntu-latest
|
|
104
|
+
# Only run if analysis-engine files changed
|
|
105
|
+
if: contains(github.event.pull_request.changed_files, 'ecip-analysis-engine/')
|
|
106
|
+
steps:
|
|
107
|
+
- uses: actions/checkout@v4
|
|
108
|
+
|
|
109
|
+
- uses: actions/setup-python@v5
|
|
110
|
+
with:
|
|
111
|
+
python-version: '3.11'
|
|
112
|
+
|
|
113
|
+
- name: Install Ruff
|
|
114
|
+
run: pip install ruff>=0.2.0
|
|
115
|
+
|
|
116
|
+
- name: Run Ruff with print() ban
|
|
117
|
+
working-directory: ecip-analysis-engine
|
|
118
|
+
run: ruff check src/ --config ../ecip-observability-stack/ci/ruff-shared.toml
|
|
119
|
+
|
|
120
|
+
# -------------------------------------------------------------------------
|
|
121
|
+
# Job 4: Alert rule validation (promtool)
|
|
122
|
+
# -------------------------------------------------------------------------
|
|
123
|
+
alert-validation:
|
|
124
|
+
name: Alert rule validation
|
|
125
|
+
runs-on: ubuntu-latest
|
|
126
|
+
if: |
|
|
127
|
+
contains(join(github.event.pull_request.changed_files, ','), 'alerts/') ||
|
|
128
|
+
github.event_name == 'push'
|
|
129
|
+
steps:
|
|
130
|
+
- uses: actions/checkout@v4
|
|
131
|
+
|
|
132
|
+
- name: Install promtool
|
|
133
|
+
run: |
|
|
134
|
+
PROM_VERSION="2.50.1"
|
|
135
|
+
wget -q "https://github.com/prometheus/prometheus/releases/download/v${PROM_VERSION}/prometheus-${PROM_VERSION}.linux-amd64.tar.gz"
|
|
136
|
+
tar xzf "prometheus-${PROM_VERSION}.linux-amd64.tar.gz"
|
|
137
|
+
sudo mv "prometheus-${PROM_VERSION}.linux-amd64/promtool" /usr/local/bin/
|
|
138
|
+
|
|
139
|
+
- name: Validate alert rules
|
|
140
|
+
run: promtool check rules ecip-observability-stack/alerts/*.yaml
|
|
141
|
+
|
|
142
|
+
# -------------------------------------------------------------------------
|
|
143
|
+
# Job 5: Dashboard JSON lint
|
|
144
|
+
# -------------------------------------------------------------------------
|
|
145
|
+
dashboard-lint:
|
|
146
|
+
name: Dashboard lint
|
|
147
|
+
runs-on: ubuntu-latest
|
|
148
|
+
if: |
|
|
149
|
+
contains(join(github.event.pull_request.changed_files, ','), 'dashboards/') ||
|
|
150
|
+
github.event_name == 'push'
|
|
151
|
+
steps:
|
|
152
|
+
- uses: actions/checkout@v4
|
|
153
|
+
|
|
154
|
+
- uses: actions/setup-node@v4
|
|
155
|
+
with:
|
|
156
|
+
node-version: '20'
|
|
157
|
+
|
|
158
|
+
- name: Lint dashboard JSON
|
|
159
|
+
run: node ecip-observability-stack/scripts/lint-dashboards.js
|
|
160
|
+
|
|
161
|
+
# -------------------------------------------------------------------------
|
|
162
|
+
# Job 6: M08 unit tests
|
|
163
|
+
# -------------------------------------------------------------------------
|
|
164
|
+
m08-tests:
|
|
165
|
+
name: M08 unit tests
|
|
166
|
+
runs-on: ubuntu-latest
|
|
167
|
+
steps:
|
|
168
|
+
- uses: actions/checkout@v4
|
|
169
|
+
|
|
170
|
+
- uses: actions/setup-node@v4
|
|
171
|
+
with:
|
|
172
|
+
node-version: '20'
|
|
173
|
+
|
|
174
|
+
- name: Install dependencies
|
|
175
|
+
working-directory: ecip-observability-stack
|
|
176
|
+
run: npm ci
|
|
177
|
+
|
|
178
|
+
- name: Run tests
|
|
179
|
+
working-directory: ecip-observability-stack
|
|
180
|
+
run: npm test
|