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.
Files changed (76) hide show
  1. package/CLAUDE.md +48 -0
  2. package/README.md +75 -0
  3. package/alerts/analysis-backlog.yaml +39 -0
  4. package/alerts/cache-degradation.yaml +44 -0
  5. package/alerts/dlq-depth.yaml +56 -0
  6. package/alerts/lsp-daemon.yaml +43 -0
  7. package/alerts/mcp-latency.yaml +46 -0
  8. package/alerts/security-anomaly.yaml +59 -0
  9. package/alerts/sla-latency.yaml +61 -0
  10. package/chaos/kafka-broker-restart.sh +168 -0
  11. package/chaos/kill-lsp-daemon.sh +148 -0
  12. package/chaos/redis-node-failure.sh +318 -0
  13. package/ci/check-observability-contract.js +285 -0
  14. package/ci/eslint-plugin-ecip/index.js +209 -0
  15. package/ci/eslint-plugin-ecip/package.json +12 -0
  16. package/ci/github-actions-observability-gate.yaml +180 -0
  17. package/ci/ruff-shared.toml +41 -0
  18. package/collector/otel-collector-config.yaml +226 -0
  19. package/collector/otel-collector-daemonset.yaml +168 -0
  20. package/collector/sampling-config.yaml +83 -0
  21. package/dashboards/_provisioning/grafana-dashboards.yaml +16 -0
  22. package/dashboards/analysis-throughput.json +166 -0
  23. package/dashboards/cache-performance.json +129 -0
  24. package/dashboards/cross-repo-fanout.json +93 -0
  25. package/dashboards/event-bus-dlq.json +129 -0
  26. package/dashboards/lsp-daemon-health.json +104 -0
  27. package/dashboards/mcp-call-graph.json +114 -0
  28. package/dashboards/query-latency.json +160 -0
  29. package/dashboards/security-events.json +131 -0
  30. package/docs/M08-Observability-Design.md +639 -0
  31. package/docs/PROGRESS.md +375 -0
  32. package/docs/module-documentation.md +64 -0
  33. package/elasticsearch/ilm-policy.json +57 -0
  34. package/elasticsearch/index-template.json +62 -0
  35. package/elasticsearch/kibana-space.yaml +53 -0
  36. package/helm/Chart.yaml +30 -0
  37. package/helm/templates/configmaps.yaml +25 -0
  38. package/helm/templates/elasticsearch.yaml +68 -0
  39. package/helm/templates/grafana-secret.yaml +22 -0
  40. package/helm/templates/grafana.yaml +19 -0
  41. package/helm/templates/loki.yaml +33 -0
  42. package/helm/templates/otel-collector.yaml +119 -0
  43. package/helm/templates/prometheus.yaml +43 -0
  44. package/helm/templates/tempo.yaml +16 -0
  45. package/helm/values.prod.yaml +159 -0
  46. package/helm/values.yaml +146 -0
  47. package/logging-lib/nodejs/package.json +57 -0
  48. package/logging-lib/nodejs/pnpm-lock.yaml +4576 -0
  49. package/logging-lib/python/pyproject.toml +45 -0
  50. package/logging-lib/python/src/__init__.py +19 -0
  51. package/logging-lib/python/src/logger.py +131 -0
  52. package/logging-lib/python/src/security_events.py +150 -0
  53. package/logging-lib/python/src/tracer.py +185 -0
  54. package/logging-lib/python/tests/test_logger.py +113 -0
  55. package/package.json +21 -0
  56. package/prometheus/prometheus-values.yaml +170 -0
  57. package/prometheus/recording-rules.yaml +97 -0
  58. package/prometheus/scrape-configs.yaml +122 -0
  59. package/runbooks/SDK-INTEGRATION.md +239 -0
  60. package/runbooks/alert-response/ANALYSIS_BACKLOG.md +128 -0
  61. package/runbooks/alert-response/DLQ_DEPTH_EXCEEDED.md +150 -0
  62. package/runbooks/alert-response/HIGH_QUERY_LATENCY.md +134 -0
  63. package/runbooks/alert-response/LSP_DAEMON_RESTART.md +118 -0
  64. package/runbooks/alert-response/SECURITY_ANOMALY.md +160 -0
  65. package/runbooks/dashboard-guide.md +169 -0
  66. package/scripts/lint-dashboards.js +184 -0
  67. package/tempo/tempo-datasource.yaml +46 -0
  68. package/tempo/tempo-values.yaml +94 -0
  69. package/tests/alert-threshold-config.test.ts +283 -0
  70. package/tests/log-schema-validation.test.ts +246 -0
  71. package/tests/metric-label-validation.test.ts +292 -0
  72. package/tests/otel-pipeline-integration.test.ts +420 -0
  73. package/tests/security-events.test.ts +417 -0
  74. package/tsconfig.json +17 -0
  75. package/vitest.config.ts +21 -0
  76. 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