@dipseth/opensearch-logs 0.1.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/.env.example +14 -0
- package/alerts/langfuse-usage.yaml +142 -0
- package/alerts/production-incidents.yaml +280 -0
- package/alerts/service-health.yaml +98 -0
- package/dashboards/langfuse-usage.yaml +57 -0
- package/dist/create-dashboards.d.ts +10 -0
- package/dist/create-dashboards.js +38 -0
- package/dist/create-dashboards.js.map +1 -0
- package/dist/interfaces/alert.interfaces.d.ts +323 -0
- package/dist/interfaces/alert.interfaces.js +6 -0
- package/dist/interfaces/alert.interfaces.js.map +1 -0
- package/dist/interfaces/dashboard-gen.interfaces.d.ts +33 -0
- package/dist/interfaces/dashboard-gen.interfaces.js +3 -0
- package/dist/interfaces/dashboard-gen.interfaces.js.map +1 -0
- package/dist/interfaces/interfaces.d.ts +312 -0
- package/dist/interfaces/interfaces.js +3 -0
- package/dist/interfaces/interfaces.js.map +1 -0
- package/dist/interfaces/playbook.interfaces.d.ts +140 -0
- package/dist/interfaces/playbook.interfaces.js +3 -0
- package/dist/interfaces/playbook.interfaces.js.map +1 -0
- package/dist/os-alert.d.ts +17 -0
- package/dist/os-alert.js +245 -0
- package/dist/os-alert.js.map +1 -0
- package/dist/os-dash.d.ts +9 -0
- package/dist/os-dash.js +53 -0
- package/dist/os-dash.js.map +1 -0
- package/dist/os-monitor.d.ts +12 -0
- package/dist/os-monitor.js +59 -0
- package/dist/os-monitor.js.map +1 -0
- package/dist/os-playbook.d.ts +9 -0
- package/dist/os-playbook.js +71 -0
- package/dist/os-playbook.js.map +1 -0
- package/dist/os-search.d.ts +11 -0
- package/dist/os-search.js +84 -0
- package/dist/os-search.js.map +1 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +2 -0
- package/dist/repositories/index.js.map +1 -0
- package/dist/repositories/opensearch.repository.d.ts +51 -0
- package/dist/repositories/opensearch.repository.js +167 -0
- package/dist/repositories/opensearch.repository.js.map +1 -0
- package/dist/services/alert.service.d.ts +73 -0
- package/dist/services/alert.service.js +503 -0
- package/dist/services/alert.service.js.map +1 -0
- package/dist/services/dashboard-gen.service.d.ts +36 -0
- package/dist/services/dashboard-gen.service.js +162 -0
- package/dist/services/dashboard-gen.service.js.map +1 -0
- package/dist/services/dashboard.service.d.ts +33 -0
- package/dist/services/dashboard.service.js +428 -0
- package/dist/services/dashboard.service.js.map +1 -0
- package/dist/services/gchat.service.d.ts +45 -0
- package/dist/services/gchat.service.js +228 -0
- package/dist/services/gchat.service.js.map +1 -0
- package/dist/services/index.d.ts +8 -0
- package/dist/services/index.js +9 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/monitor.service.d.ts +18 -0
- package/dist/services/monitor.service.js +342 -0
- package/dist/services/monitor.service.js.map +1 -0
- package/dist/services/panel-layout.d.ts +21 -0
- package/dist/services/panel-layout.js +33 -0
- package/dist/services/panel-layout.js.map +1 -0
- package/dist/services/playbook-dashboard.service.d.ts +19 -0
- package/dist/services/playbook-dashboard.service.js +434 -0
- package/dist/services/playbook-dashboard.service.js.map +1 -0
- package/dist/services/playbook.service.d.ts +13 -0
- package/dist/services/playbook.service.js +621 -0
- package/dist/services/playbook.service.js.map +1 -0
- package/dist/services/search.service.d.ts +30 -0
- package/dist/services/search.service.js +885 -0
- package/dist/services/search.service.js.map +1 -0
- package/dist/utils/cli.d.ts +14 -0
- package/dist/utils/cli.js +90 -0
- package/dist/utils/cli.js.map +1 -0
- package/dist/utils/config.d.ts +20 -0
- package/dist/utils/config.js +104 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/service-registry.d.ts +15 -0
- package/dist/utils/service-registry.js +56 -0
- package/dist/utils/service-registry.js.map +1 -0
- package/dist/utils/template.d.ts +18 -0
- package/dist/utils/template.js +66 -0
- package/dist/utils/template.js.map +1 -0
- package/package.json +76 -0
- package/playbooks/error-investigation.yaml +45 -0
- package/playbooks/incident-triage.yaml +32 -0
- package/playbooks/post-deploy-validation.yaml +24 -0
- package/playbooks/service-deep-dive.yaml +42 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML-driven dashboard generator — creates OSD saved dashboards
|
|
3
|
+
* from a declarative config file. No code required.
|
|
4
|
+
*
|
|
5
|
+
* Supports: header markdown, metric tiles, line charts, markdown sections.
|
|
6
|
+
* Layout is automatic: metrics fill rows of 6, charts pair half-width,
|
|
7
|
+
* full-width charts get their own row.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { parse as parseYaml } from "yaml";
|
|
13
|
+
import { visMarkdown, visMetric, visLineChart } from "./dashboard.service.js";
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
const DASHBOARDS_DIR = join(__dirname, "..", "..", "dashboards");
|
|
17
|
+
const GRID_W = 48;
|
|
18
|
+
const HALF_W = 24;
|
|
19
|
+
const METRIC_W = 8;
|
|
20
|
+
const HEADER_H = 4;
|
|
21
|
+
const METRIC_H = 6;
|
|
22
|
+
const CHART_H = 12;
|
|
23
|
+
function slug(name) {
|
|
24
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
25
|
+
}
|
|
26
|
+
export class DashboardGenService {
|
|
27
|
+
repo;
|
|
28
|
+
constructor(repo) {
|
|
29
|
+
this.repo = repo;
|
|
30
|
+
}
|
|
31
|
+
/** Load and parse a dashboard config YAML. */
|
|
32
|
+
loadConfig(configPath) {
|
|
33
|
+
// Try path as-is, then in dashboards dir
|
|
34
|
+
const candidates = [
|
|
35
|
+
configPath,
|
|
36
|
+
`${configPath}.yaml`,
|
|
37
|
+
join(DASHBOARDS_DIR, configPath),
|
|
38
|
+
join(DASHBOARDS_DIR, `${configPath}.yaml`),
|
|
39
|
+
];
|
|
40
|
+
for (const path of candidates) {
|
|
41
|
+
if (existsSync(path)) {
|
|
42
|
+
return parseYaml(readFileSync(path, "utf-8"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Dashboard config not found: ${configPath}\nSearched: ${candidates.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build all visualizations and the dashboard layout from config.
|
|
49
|
+
* Returns the vis list, panel layout, and references — ready for upsert.
|
|
50
|
+
*/
|
|
51
|
+
build(config) {
|
|
52
|
+
const prefix = slug(config.name);
|
|
53
|
+
const dashId = `custom-${prefix}`;
|
|
54
|
+
const vizList = [];
|
|
55
|
+
const layout = [];
|
|
56
|
+
// ── Header ─────────────────────────────────────────────────────
|
|
57
|
+
if (config.header) {
|
|
58
|
+
const visId = `${prefix}-header`;
|
|
59
|
+
vizList.push(visMarkdown(config.title + " Header", visId, config.header));
|
|
60
|
+
layout.push({ visId, w: GRID_W, h: HEADER_H });
|
|
61
|
+
}
|
|
62
|
+
// ── Metrics (auto-flow row of up to 6) ─────────────────────────
|
|
63
|
+
if (config.metrics) {
|
|
64
|
+
for (const m of config.metrics) {
|
|
65
|
+
const visId = `${prefix}-m-${slug(m.title)}`;
|
|
66
|
+
vizList.push(visMetric(m.title, visId, m.query, m.label ?? m.title));
|
|
67
|
+
layout.push({ visId, w: METRIC_W, h: METRIC_H });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ── Charts ─────────────────────────────────────────────────────
|
|
71
|
+
if (config.charts) {
|
|
72
|
+
for (const c of config.charts) {
|
|
73
|
+
const visId = `${prefix}-c-${slug(c.title)}`;
|
|
74
|
+
vizList.push(visLineChart(c.title, visId, c.series, c.interval));
|
|
75
|
+
const w = c.width === "full" ? GRID_W : HALF_W;
|
|
76
|
+
layout.push({ visId, w, h: CHART_H });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ── Markdown sections ──────────────────────────────────────────
|
|
80
|
+
if (config.markdown) {
|
|
81
|
+
for (const md of config.markdown) {
|
|
82
|
+
const visId = `${prefix}-md-${slug(md.title)}`;
|
|
83
|
+
const lines = md.text.split("\n").length;
|
|
84
|
+
const h = Math.max(4, Math.min(24, Math.ceil(lines * 1.5) + 2));
|
|
85
|
+
vizList.push(visMarkdown(md.title, visId, md.text));
|
|
86
|
+
layout.push({ visId, w: GRID_W, h });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ── Auto-layout ────────────────────────────────────────────────
|
|
90
|
+
let x = 0;
|
|
91
|
+
let y = 0;
|
|
92
|
+
let rowHeight = 0;
|
|
93
|
+
const panels = [];
|
|
94
|
+
const refs = [];
|
|
95
|
+
for (let i = 0; i < layout.length; i++) {
|
|
96
|
+
const { visId, w, h } = layout[i];
|
|
97
|
+
if (x + w > GRID_W) {
|
|
98
|
+
x = 0;
|
|
99
|
+
y += rowHeight;
|
|
100
|
+
rowHeight = 0;
|
|
101
|
+
}
|
|
102
|
+
const pid = `panel_${i}`;
|
|
103
|
+
panels.push({
|
|
104
|
+
version: "2.17.1",
|
|
105
|
+
gridData: { x, y, w, h, i: pid },
|
|
106
|
+
panelIndex: pid,
|
|
107
|
+
embeddableConfig: {},
|
|
108
|
+
panelRefName: pid,
|
|
109
|
+
});
|
|
110
|
+
refs.push({ name: pid, type: "visualization", id: visId });
|
|
111
|
+
x += w;
|
|
112
|
+
rowHeight = Math.max(rowHeight, h);
|
|
113
|
+
}
|
|
114
|
+
return { vizList, panels, refs, dashId };
|
|
115
|
+
}
|
|
116
|
+
/** Create or update the dashboard on OSD. */
|
|
117
|
+
async create(config) {
|
|
118
|
+
const { vizList, panels, refs, dashId } = this.build(config);
|
|
119
|
+
const host = this.repo.config.host;
|
|
120
|
+
const tenant = this.repo.config.tenant ?? "global";
|
|
121
|
+
// Upsert visualizations
|
|
122
|
+
let vizOk = 0;
|
|
123
|
+
for (const [visId, visBody] of vizList) {
|
|
124
|
+
const [, success] = await this.repo.upsertSavedObject("visualization", visId, visBody);
|
|
125
|
+
if (success)
|
|
126
|
+
vizOk++;
|
|
127
|
+
}
|
|
128
|
+
// Upsert dashboard
|
|
129
|
+
const dashBody = {
|
|
130
|
+
attributes: {
|
|
131
|
+
title: config.title,
|
|
132
|
+
hits: 0,
|
|
133
|
+
description: config.description ?? "",
|
|
134
|
+
panelsJSON: JSON.stringify(panels),
|
|
135
|
+
optionsJSON: JSON.stringify({ useMargins: true, hidePanelTitles: false }),
|
|
136
|
+
timeRestore: true,
|
|
137
|
+
timeTo: config.time_to ?? "now",
|
|
138
|
+
timeFrom: config.time_from ?? "now-1h",
|
|
139
|
+
refreshInterval: { pause: false, value: config.refresh_interval ?? 60000 },
|
|
140
|
+
kibanaSavedObjectMeta: {
|
|
141
|
+
searchSourceJSON: JSON.stringify({ query: { query: "", language: "kuery" }, filter: [] }),
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
references: refs,
|
|
145
|
+
};
|
|
146
|
+
const [, dashOk] = await this.repo.upsertSavedObject("dashboard", dashId, dashBody);
|
|
147
|
+
const url = `https://${host}/app/dashboards?security_tenant=${tenant}#/view/${dashId}`;
|
|
148
|
+
return { url, vizOk, vizTotal: vizList.length, dashOk };
|
|
149
|
+
}
|
|
150
|
+
/** Delete a dashboard and its visualizations. */
|
|
151
|
+
async delete(config) {
|
|
152
|
+
const { vizList, dashId } = this.build(config);
|
|
153
|
+
await this.repo.deleteSavedObject("dashboard", dashId);
|
|
154
|
+
let deleted = 1;
|
|
155
|
+
for (const [visId] of vizList) {
|
|
156
|
+
await this.repo.deleteSavedObject("visualization", visId);
|
|
157
|
+
deleted++;
|
|
158
|
+
}
|
|
159
|
+
return deleted;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=dashboard-gen.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-gen.service.js","sourceRoot":"","sources":["../../src/services/dashboard-gen.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard service — creates, validates, and deletes OSD saved objects.
|
|
3
|
+
*
|
|
4
|
+
* KEY LESSONS (OSD 2.17.1):
|
|
5
|
+
* - searchSourceJSON must use "indexRefName" (not "index") to reference the pattern
|
|
6
|
+
* - Line charts: count(id=1) + date_histogram(id=2) + filters(id=3)
|
|
7
|
+
* - seriesParams reference the count agg (id=1), filters auto-split the series
|
|
8
|
+
* - Filter inputs need {"query": "...", "language": "lucene"} format
|
|
9
|
+
* - Global tenant (securitytenant: global) on all Dashboards API calls
|
|
10
|
+
*/
|
|
11
|
+
import type { OpenSearchRepository } from "../repositories/index.js";
|
|
12
|
+
import type { VisDef, SavedObjectRef } from "../interfaces/interfaces.js";
|
|
13
|
+
export declare const INDEX_PATTERN_ID = "python-services-production-v2";
|
|
14
|
+
export declare const INDEX_PATTERN_TITLE = "python-services-production-*";
|
|
15
|
+
export declare function searchSource(withQuery?: string): string;
|
|
16
|
+
export declare function indexPatternRef(): SavedObjectRef;
|
|
17
|
+
export declare function visMarkdown(title: string, visId: string, markdownText: string): VisDef;
|
|
18
|
+
export declare function visMetric(title: string, visId: string, query: string, label?: string): VisDef;
|
|
19
|
+
export declare function visLineChart(title: string, visId: string, queries: Record<string, string>, interval?: string): VisDef;
|
|
20
|
+
export declare class DashboardService {
|
|
21
|
+
private repo;
|
|
22
|
+
private dashId;
|
|
23
|
+
private dashTitle;
|
|
24
|
+
constructor(repo: OpenSearchRepository, name?: string);
|
|
25
|
+
private fetchFieldMappings;
|
|
26
|
+
private buildFieldDefinitions;
|
|
27
|
+
private ensureIndexPattern;
|
|
28
|
+
private setDefaultIndexPattern;
|
|
29
|
+
private buildDashboard;
|
|
30
|
+
create(): Promise<number>;
|
|
31
|
+
validate(): Promise<number>;
|
|
32
|
+
delete(): Promise<number>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard service — creates, validates, and deletes OSD saved objects.
|
|
3
|
+
*
|
|
4
|
+
* KEY LESSONS (OSD 2.17.1):
|
|
5
|
+
* - searchSourceJSON must use "indexRefName" (not "index") to reference the pattern
|
|
6
|
+
* - Line charts: count(id=1) + date_histogram(id=2) + filters(id=3)
|
|
7
|
+
* - seriesParams reference the count agg (id=1), filters auto-split the series
|
|
8
|
+
* - Filter inputs need {"query": "...", "language": "lucene"} format
|
|
9
|
+
* - Global tenant (securitytenant: global) on all Dashboards API calls
|
|
10
|
+
*/
|
|
11
|
+
// ── Constants ─────────────────────────────────────────────────────────────
|
|
12
|
+
export const INDEX_PATTERN_ID = "python-services-production-v2";
|
|
13
|
+
export const INDEX_PATTERN_TITLE = "python-services-production-*";
|
|
14
|
+
const DEFAULT_DASHBOARD_NAME = "v2";
|
|
15
|
+
const TENANT = "global";
|
|
16
|
+
function dashboardId(name) {
|
|
17
|
+
return `python-services-production-health-${name}`;
|
|
18
|
+
}
|
|
19
|
+
function dashboardTitle(name) {
|
|
20
|
+
return `Python Services - Production Health ${name}`;
|
|
21
|
+
}
|
|
22
|
+
// ── Field Mapping Types ──────────────────────────────────────────────────
|
|
23
|
+
const ES_TO_OSD_TYPE = {
|
|
24
|
+
text: "string", keyword: "string",
|
|
25
|
+
long: "number", integer: "number", float: "number", double: "number",
|
|
26
|
+
date: "date", boolean: "boolean",
|
|
27
|
+
};
|
|
28
|
+
const AGGREGATABLE_TYPES = new Set(["keyword", "long", "integer", "float", "double", "date", "boolean"]);
|
|
29
|
+
// ── Visualization Builders ───────────────────────────────────────────────
|
|
30
|
+
export function searchSource(withQuery) {
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
query: { query: withQuery ?? "", language: "lucene" },
|
|
33
|
+
filter: [],
|
|
34
|
+
indexRefName: "kibanaSavedObjectMeta.searchSourceJSON.index",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function indexPatternRef() {
|
|
38
|
+
return {
|
|
39
|
+
name: "kibanaSavedObjectMeta.searchSourceJSON.index",
|
|
40
|
+
type: "index-pattern",
|
|
41
|
+
id: INDEX_PATTERN_ID,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function visMarkdown(title, visId, markdownText) {
|
|
45
|
+
return [visId, {
|
|
46
|
+
attributes: {
|
|
47
|
+
title,
|
|
48
|
+
visState: JSON.stringify({
|
|
49
|
+
title, type: "markdown", aggs: [],
|
|
50
|
+
params: { markdown: markdownText, fontSize: 12 },
|
|
51
|
+
}),
|
|
52
|
+
uiStateJSON: "{}",
|
|
53
|
+
description: "",
|
|
54
|
+
kibanaSavedObjectMeta: { searchSourceJSON: searchSource() },
|
|
55
|
+
},
|
|
56
|
+
references: [indexPatternRef()],
|
|
57
|
+
}];
|
|
58
|
+
}
|
|
59
|
+
export function visMetric(title, visId, query, label = "Count") {
|
|
60
|
+
return [visId, {
|
|
61
|
+
attributes: {
|
|
62
|
+
title,
|
|
63
|
+
visState: JSON.stringify({
|
|
64
|
+
title, type: "metric",
|
|
65
|
+
aggs: [{
|
|
66
|
+
id: "1", enabled: true, type: "count",
|
|
67
|
+
schema: "metric", params: { customLabel: label },
|
|
68
|
+
}],
|
|
69
|
+
params: {
|
|
70
|
+
addTooltip: true, addLegend: false, type: "metric",
|
|
71
|
+
metric: {
|
|
72
|
+
percentageMode: false, colorSchema: "Green to Red",
|
|
73
|
+
metricColorMode: "None",
|
|
74
|
+
style: { bgFill: "#000", bgColor: false, labelColor: false, subText: "", fontSize: 60 },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
uiStateJSON: "{}",
|
|
79
|
+
description: "",
|
|
80
|
+
kibanaSavedObjectMeta: { searchSourceJSON: searchSource(query) },
|
|
81
|
+
},
|
|
82
|
+
references: [indexPatternRef()],
|
|
83
|
+
}];
|
|
84
|
+
}
|
|
85
|
+
export function visLineChart(title, visId, queries, interval = "5m") {
|
|
86
|
+
const visState = {
|
|
87
|
+
title, type: "line",
|
|
88
|
+
aggs: [
|
|
89
|
+
{ id: "1", enabled: true, type: "count", schema: "metric", params: {} },
|
|
90
|
+
{
|
|
91
|
+
id: "2", enabled: true, type: "date_histogram", schema: "segment",
|
|
92
|
+
params: {
|
|
93
|
+
field: "@timestamp", useNormalizedOpenSearchInterval: true,
|
|
94
|
+
interval, min_doc_count: 0,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "3", enabled: true, type: "filters", schema: "group",
|
|
99
|
+
params: {
|
|
100
|
+
filters: Object.entries(queries).map(([lbl, q]) => ({
|
|
101
|
+
input: { query: q, language: "lucene" }, label: lbl,
|
|
102
|
+
})),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
params: {
|
|
107
|
+
type: "line",
|
|
108
|
+
grid: { categoryLines: false },
|
|
109
|
+
categoryAxes: [{
|
|
110
|
+
id: "CategoryAxis-1", type: "category", position: "bottom", show: true,
|
|
111
|
+
labels: { show: true, truncate: 100 },
|
|
112
|
+
}],
|
|
113
|
+
valueAxes: [{
|
|
114
|
+
id: "ValueAxis-1", name: "LeftAxis-1", type: "value", position: "left", show: true,
|
|
115
|
+
labels: { show: true },
|
|
116
|
+
}],
|
|
117
|
+
seriesParams: [{
|
|
118
|
+
show: true, type: "line", mode: "normal",
|
|
119
|
+
data: { label: "Count", id: "1" },
|
|
120
|
+
valueAxis: "ValueAxis-1",
|
|
121
|
+
drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true,
|
|
122
|
+
}],
|
|
123
|
+
addTooltip: true, addLegend: true, legendPosition: "right",
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
return [visId, {
|
|
127
|
+
attributes: {
|
|
128
|
+
title,
|
|
129
|
+
visState: JSON.stringify(visState),
|
|
130
|
+
uiStateJSON: "{}",
|
|
131
|
+
description: "",
|
|
132
|
+
kibanaSavedObjectMeta: { searchSourceJSON: searchSource() },
|
|
133
|
+
},
|
|
134
|
+
references: [indexPatternRef()],
|
|
135
|
+
}];
|
|
136
|
+
}
|
|
137
|
+
// ── Dashboard Content ─────────────────────────────────────────────────────
|
|
138
|
+
const VISUALIZATIONS = [
|
|
139
|
+
visMarkdown("Production Health - Header", "prod-health-header", "## Python Services - Production Health\n\n"
|
|
140
|
+
+ "Monitoring for all 15 FastAPI microservices on the shared DO droplet.\n"
|
|
141
|
+
+ "Use time picker (top right) to adjust window. Auto-refreshes every 60s."),
|
|
142
|
+
visMetric("Total 500s", "prod-metric-500s", '"500 Internal Server Error"', "500 Errors"),
|
|
143
|
+
visMetric("Total 503s", "prod-metric-503s", "status=503", "503 Unavailable"),
|
|
144
|
+
visMetric("Connection Refused", "prod-metric-conn-refused", '"Connection refused"', "Conn Refused"),
|
|
145
|
+
visMetric("Worker Shutdowns", "prod-metric-shutdowns", '"Shutting down"', "Shutdowns"),
|
|
146
|
+
visMetric("Playwright Failures", "prod-metric-playwright", '"Failed to launch browser"', "Playwright"),
|
|
147
|
+
visMetric("API Key Missing", "prod-metric-apikey", '"ENCORE_G_API_KEY: None" OR "ENCORE_G_API_KEY: NOT SET"', "API Key None"),
|
|
148
|
+
visLineChart("HTTP Errors Over Time", "prod-http-errors-timeline", {
|
|
149
|
+
"500 Internal Server Error": '"500 Internal Server Error"',
|
|
150
|
+
"503 Service Unavailable": "status=503",
|
|
151
|
+
"502 Bad Gateway": "status=502",
|
|
152
|
+
"499 Client Timeout": "status=499",
|
|
153
|
+
}),
|
|
154
|
+
visLineChart("Worker Lifecycle Events", "prod-worker-lifecycle", {
|
|
155
|
+
Shutdowns: '"Shutting down"',
|
|
156
|
+
Startups: '"Application startup complete"',
|
|
157
|
+
"Max Request Limit": '"Maximum request limit"',
|
|
158
|
+
}),
|
|
159
|
+
visLineChart("Infrastructure Issues", "prod-infra-issues", {
|
|
160
|
+
"Connection Refused": '"Connection refused"',
|
|
161
|
+
"Playwright Failures": '"Failed to launch browser"',
|
|
162
|
+
"OOM / Memory": 'OOM OR "Cannot allocate memory"',
|
|
163
|
+
}),
|
|
164
|
+
visLineChart("Deal Structure - Request Status", "prod-deal-structure-status", {
|
|
165
|
+
"200 OK": "deal_structure AND status=200",
|
|
166
|
+
"422 Validation": "deal_structure AND status=422",
|
|
167
|
+
"500 Error": 'deal_structure AND "500"',
|
|
168
|
+
"502 Bad Gateway": "deal_structure AND status=502",
|
|
169
|
+
"Connection Refused": '"Connection refused" AND deal_structure',
|
|
170
|
+
}),
|
|
171
|
+
visLineChart("503s by Service Path", "prod-503-by-service", {
|
|
172
|
+
inferpds_v5: "status=503 AND inferpds_v5",
|
|
173
|
+
deal_structure: "status=503 AND deal_structure",
|
|
174
|
+
merchant_quality: "status=503 AND merchant_quality",
|
|
175
|
+
inferpds_unified: "status=503 AND inferpds_unified",
|
|
176
|
+
other: "status=503 NOT inferpds_v5 NOT deal_structure NOT merchant_quality NOT inferpds_unified",
|
|
177
|
+
}),
|
|
178
|
+
];
|
|
179
|
+
const PANEL_LAYOUT = [
|
|
180
|
+
["prod-health-header", 0, 0, 48, 4],
|
|
181
|
+
["prod-metric-500s", 0, 4, 8, 6],
|
|
182
|
+
["prod-metric-503s", 8, 4, 8, 6],
|
|
183
|
+
["prod-metric-conn-refused", 16, 4, 8, 6],
|
|
184
|
+
["prod-metric-shutdowns", 24, 4, 8, 6],
|
|
185
|
+
["prod-metric-playwright", 32, 4, 8, 6],
|
|
186
|
+
["prod-metric-apikey", 40, 4, 8, 6],
|
|
187
|
+
["prod-http-errors-timeline", 0, 10, 24, 12],
|
|
188
|
+
["prod-worker-lifecycle", 24, 10, 24, 12],
|
|
189
|
+
["prod-infra-issues", 0, 22, 24, 12],
|
|
190
|
+
["prod-deal-structure-status", 24, 22, 24, 12],
|
|
191
|
+
["prod-503-by-service", 0, 34, 48, 12],
|
|
192
|
+
];
|
|
193
|
+
// ── Service Class ─────────────────────────────────────────────────────────
|
|
194
|
+
export class DashboardService {
|
|
195
|
+
repo;
|
|
196
|
+
dashId;
|
|
197
|
+
dashTitle;
|
|
198
|
+
constructor(repo, name) {
|
|
199
|
+
this.repo = repo;
|
|
200
|
+
const n = name ?? DEFAULT_DASHBOARD_NAME;
|
|
201
|
+
this.dashId = dashboardId(n);
|
|
202
|
+
this.dashTitle = dashboardTitle(n);
|
|
203
|
+
}
|
|
204
|
+
async fetchFieldMappings() {
|
|
205
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, ".");
|
|
206
|
+
let result = await this.repo.dataApi("GET", `/python-services-production-${today}/_mapping`);
|
|
207
|
+
if (!result || result._error) {
|
|
208
|
+
result = await this.repo.dataApi("GET", "/python-services-production-*/_mapping");
|
|
209
|
+
}
|
|
210
|
+
if (!result || result._error)
|
|
211
|
+
return null;
|
|
212
|
+
const mappingResult = result;
|
|
213
|
+
for (const [idxName, idxData] of Object.entries(mappingResult)) {
|
|
214
|
+
if (idxName.startsWith("_"))
|
|
215
|
+
continue;
|
|
216
|
+
return idxData.mappings?.properties ?? null;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
buildFieldDefinitions(properties) {
|
|
221
|
+
const fields = [];
|
|
222
|
+
for (const [name, spec] of Object.entries(properties)) {
|
|
223
|
+
const esType = spec.type ?? "object";
|
|
224
|
+
const osdType = ES_TO_OSD_TYPE[esType] ?? "string";
|
|
225
|
+
fields.push({
|
|
226
|
+
name, type: osdType, esTypes: [esType],
|
|
227
|
+
searchable: true,
|
|
228
|
+
aggregatable: AGGREGATABLE_TYPES.has(esType),
|
|
229
|
+
readFromDocValues: esType !== "text",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
fields.push({ name: "_source", type: "_source", esTypes: ["_source"], searchable: false, aggregatable: false, readFromDocValues: false });
|
|
233
|
+
fields.push({ name: "_id", type: "string", esTypes: ["_id"], searchable: true, aggregatable: false, readFromDocValues: false });
|
|
234
|
+
return fields;
|
|
235
|
+
}
|
|
236
|
+
async ensureIndexPattern() {
|
|
237
|
+
const properties = await this.fetchFieldMappings();
|
|
238
|
+
let fieldsJson = null;
|
|
239
|
+
if (!properties) {
|
|
240
|
+
console.log(" WARN: Could not fetch field mappings. Creating pattern without fields.");
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
fieldsJson = JSON.stringify(this.buildFieldDefinitions(properties));
|
|
244
|
+
}
|
|
245
|
+
const attrs = { title: INDEX_PATTERN_TITLE, timeFieldName: "@timestamp" };
|
|
246
|
+
if (fieldsJson)
|
|
247
|
+
attrs.fields = fieldsJson;
|
|
248
|
+
await this.repo.deleteSavedObject("index-pattern", INDEX_PATTERN_ID);
|
|
249
|
+
const result = await this.repo.dashboardsApi("POST", `/api/saved_objects/index-pattern/${INDEX_PATTERN_ID}`, { attributes: attrs });
|
|
250
|
+
if (result && !result._error) {
|
|
251
|
+
return [true, fieldsJson ? JSON.parse(fieldsJson).length : 0];
|
|
252
|
+
}
|
|
253
|
+
return [false, 0];
|
|
254
|
+
}
|
|
255
|
+
async setDefaultIndexPattern() {
|
|
256
|
+
const result = await this.repo.dashboardsApi("POST", "/api/opensearch-dashboards/settings", {
|
|
257
|
+
changes: { defaultIndex: INDEX_PATTERN_ID },
|
|
258
|
+
});
|
|
259
|
+
return !!result && !result._error;
|
|
260
|
+
}
|
|
261
|
+
buildDashboard() {
|
|
262
|
+
const panels = [];
|
|
263
|
+
const references = [];
|
|
264
|
+
for (let i = 0; i < PANEL_LAYOUT.length; i++) {
|
|
265
|
+
const [visId, x, y, w, h] = PANEL_LAYOUT[i];
|
|
266
|
+
const pid = `panel_${i}`;
|
|
267
|
+
panels.push({
|
|
268
|
+
version: "2.17.1",
|
|
269
|
+
gridData: { x, y, w, h, i: pid },
|
|
270
|
+
panelIndex: pid,
|
|
271
|
+
embeddableConfig: {},
|
|
272
|
+
panelRefName: pid,
|
|
273
|
+
});
|
|
274
|
+
references.push({ name: pid, type: "visualization", id: visId });
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
attributes: {
|
|
278
|
+
title: this.dashTitle, hits: 0,
|
|
279
|
+
description: "Production health monitoring for all Python microservices on the shared DO droplet",
|
|
280
|
+
panelsJSON: JSON.stringify(panels),
|
|
281
|
+
optionsJSON: JSON.stringify({ useMargins: true, hidePanelTitles: false }),
|
|
282
|
+
timeRestore: true, timeTo: "now", timeFrom: "now-1h",
|
|
283
|
+
refreshInterval: { pause: false, value: 60000 },
|
|
284
|
+
kibanaSavedObjectMeta: {
|
|
285
|
+
searchSourceJSON: JSON.stringify({ query: { query: "", language: "kuery" }, filter: [] }),
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
references,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// ── Public Actions ──────────────────────────────────────────────────
|
|
292
|
+
async create() {
|
|
293
|
+
const host = this.repo.config.host;
|
|
294
|
+
console.log(`\n${"=".repeat(64)}`);
|
|
295
|
+
console.log(` Creating: ${this.dashTitle}`);
|
|
296
|
+
console.log(` Tenant: ${TENANT} | Host: ${host}`);
|
|
297
|
+
console.log(`${"=".repeat(64)}\n`);
|
|
298
|
+
console.log(" [1/4] Index pattern with field mappings...");
|
|
299
|
+
const [ipOk, fieldCount] = await this.ensureIndexPattern();
|
|
300
|
+
console.log(ipOk ? ` created (${fieldCount} fields from live index)` : " FAILED");
|
|
301
|
+
process.stdout.write(" [2/4] Setting default index pattern... ");
|
|
302
|
+
console.log(await this.setDefaultIndexPattern() ? "done" : "FAILED");
|
|
303
|
+
console.log(` [3/4] Visualizations (${VISUALIZATIONS.length})...`);
|
|
304
|
+
let visOk = 0;
|
|
305
|
+
for (const [visId, visBody] of VISUALIZATIONS) {
|
|
306
|
+
const title = String(visBody.attributes.title ?? "");
|
|
307
|
+
const [action, success] = await this.repo.upsertSavedObject("visualization", visId, visBody);
|
|
308
|
+
if (success)
|
|
309
|
+
visOk++;
|
|
310
|
+
console.log(` ${(success ? action : "FAILED").padEnd(10)} ${title}`);
|
|
311
|
+
}
|
|
312
|
+
process.stdout.write(" [4/4] Dashboard... ");
|
|
313
|
+
const [action, success] = await this.repo.upsertSavedObject("dashboard", this.dashId, this.buildDashboard());
|
|
314
|
+
console.log(success ? action : "FAILED");
|
|
315
|
+
const url = `https://${host}/app/dashboards?security_tenant=global#/view/${this.dashId}`;
|
|
316
|
+
console.log(`\n Result: ${visOk}/${VISUALIZATIONS.length} visualizations, dashboard ${success ? "OK" : "FAILED"}`);
|
|
317
|
+
console.log(`\n Dashboard URL:\n ${url}\n`);
|
|
318
|
+
return visOk === VISUALIZATIONS.length && success ? 0 : 1;
|
|
319
|
+
}
|
|
320
|
+
async validate() {
|
|
321
|
+
const host = this.repo.config.host;
|
|
322
|
+
console.log(`\n${"=".repeat(64)}`);
|
|
323
|
+
console.log(` Validating: ${this.dashTitle}`);
|
|
324
|
+
console.log(` Tenant: ${TENANT} | Host: ${host}`);
|
|
325
|
+
console.log(`${"=".repeat(64)}\n`);
|
|
326
|
+
const issues = [];
|
|
327
|
+
process.stdout.write(" [1/5] OpenSearch data API connectivity... ");
|
|
328
|
+
const health = await this.repo.dataApi("GET", "/_cluster/health");
|
|
329
|
+
if (health && !health._error) {
|
|
330
|
+
console.log(`OK (status: ${health.status ?? "?"}`);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
console.log("FAILED");
|
|
334
|
+
issues.push("Cannot connect to OpenSearch data API on port 25060");
|
|
335
|
+
}
|
|
336
|
+
process.stdout.write(" [2/5] OpenSearch Dashboards API connectivity... ");
|
|
337
|
+
const status = await this.repo.dashboardsApi("GET", "/api/status");
|
|
338
|
+
if (status?.version) {
|
|
339
|
+
const ver = status.version.number ?? "?";
|
|
340
|
+
console.log(`OK (version: ${ver})`);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
console.log("FAILED");
|
|
344
|
+
issues.push("Cannot connect to OpenSearch Dashboards API on port 443");
|
|
345
|
+
}
|
|
346
|
+
process.stdout.write(" [3/5] Index pattern... ");
|
|
347
|
+
const ipResult = await this.repo.getSavedObject("index-pattern", INDEX_PATTERN_ID);
|
|
348
|
+
if (ipResult) {
|
|
349
|
+
const so = ipResult;
|
|
350
|
+
const fieldsRaw = so.attributes?.fields;
|
|
351
|
+
const fCount = fieldsRaw ? JSON.parse(fieldsRaw).length : 0;
|
|
352
|
+
if (fCount > 0) {
|
|
353
|
+
console.log(`OK (${fCount} fields)`);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
console.log("EXISTS but NO FIELD MAPPINGS (visualizations will fail)");
|
|
357
|
+
issues.push("Index pattern has no field mappings — run without --validate to recreate");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.log("MISSING");
|
|
362
|
+
issues.push("Index pattern not found — run without --validate to create");
|
|
363
|
+
}
|
|
364
|
+
console.log(` [4/5] Visualizations (${VISUALIZATIONS.length})...`);
|
|
365
|
+
let visOk = 0;
|
|
366
|
+
for (const [visId, visBody] of VISUALIZATIONS) {
|
|
367
|
+
const title = String(visBody.attributes.title ?? "");
|
|
368
|
+
const vResult = await this.repo.getSavedObject("visualization", visId);
|
|
369
|
+
if (vResult) {
|
|
370
|
+
const vSo = vResult;
|
|
371
|
+
const ss = JSON.parse(vSo.attributes?.kibanaSavedObjectMeta?.searchSourceJSON ?? "{}");
|
|
372
|
+
if ("indexRefName" in ss) {
|
|
373
|
+
console.log(` OK ${title}`);
|
|
374
|
+
visOk++;
|
|
375
|
+
}
|
|
376
|
+
else if ("index" in ss) {
|
|
377
|
+
console.log(` BROKEN ${title} (uses 'index' instead of 'indexRefName')`);
|
|
378
|
+
issues.push(`Visualization '${title}' uses old reference format — recreate to fix`);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
console.log(` WARN ${title} (no index reference)`);
|
|
382
|
+
visOk++;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
console.log(` MISSING ${title}`);
|
|
387
|
+
issues.push(`Visualization '${title}' is missing`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
process.stdout.write(" [5/5] Dashboard... ");
|
|
391
|
+
const dResult = await this.repo.getSavedObject("dashboard", this.dashId);
|
|
392
|
+
if (dResult) {
|
|
393
|
+
const dSo = dResult;
|
|
394
|
+
const panelCount = JSON.parse(dSo.attributes?.panelsJSON ?? "[]").length;
|
|
395
|
+
const refs = dSo.references ?? [];
|
|
396
|
+
console.log(`OK (${panelCount} panels, ${refs.length} references)`);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
console.log("MISSING");
|
|
400
|
+
issues.push("Dashboard not found — run without --validate to create");
|
|
401
|
+
}
|
|
402
|
+
console.log(`\n ${"─".repeat(40)}`);
|
|
403
|
+
if (issues.length === 0) {
|
|
404
|
+
console.log(" All checks passed.");
|
|
405
|
+
console.log(`\n Dashboard URL:\n https://${host}/app/dashboards?security_tenant=global#/view/${this.dashId}\n`);
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
console.log(` ${issues.length} issue(s) found:\n`);
|
|
409
|
+
for (const issue of issues)
|
|
410
|
+
console.log(` - ${issue}`);
|
|
411
|
+
console.log("\n Run without --validate to fix:\n node dist/create-dashboards.js\n");
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
async delete() {
|
|
415
|
+
console.log(`\nDeleting dashboard objects from ${TENANT} tenant...\n`);
|
|
416
|
+
await this.repo.deleteSavedObject("dashboard", this.dashId);
|
|
417
|
+
console.log(` Deleted dashboard: ${this.dashId}`);
|
|
418
|
+
for (const [visId] of VISUALIZATIONS) {
|
|
419
|
+
await this.repo.deleteSavedObject("visualization", visId);
|
|
420
|
+
console.log(` Deleted visualization: ${visId}`);
|
|
421
|
+
}
|
|
422
|
+
await this.repo.deleteSavedObject("index-pattern", INDEX_PATTERN_ID);
|
|
423
|
+
console.log(` Deleted index pattern: ${INDEX_PATTERN_ID}`);
|
|
424
|
+
console.log("\n Done.\n");
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
//# sourceMappingURL=dashboard.service.js.map
|