@loghead/core 0.1.33 ā 0.1.35
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/dist/api/controllers.js +106 -0
- package/dist/api/server.js +82 -113
- package/dist/public/assets/index-Bhs7GffT.js +203 -0
- package/dist/public/assets/index-DNKOkq6D.css +1 -0
- package/dist/public/index.html +2 -2
- package/package.json +1 -1
- package/dist/public/assets/index-BFN5xtu_.js +0 -164
- package/dist/public/assets/index-PEkusdx8.css +0 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Helper to parse OTLP attributes
|
|
2
|
+
export const parseOtlpAttributes = (attributes) => {
|
|
3
|
+
if (!Array.isArray(attributes))
|
|
4
|
+
return {};
|
|
5
|
+
const result = {};
|
|
6
|
+
for (const attr of attributes) {
|
|
7
|
+
if (attr.key && attr.value) {
|
|
8
|
+
// Extract value based on type (stringValue, intValue, boolValue, etc.)
|
|
9
|
+
const val = attr.value;
|
|
10
|
+
if (val.stringValue !== undefined)
|
|
11
|
+
result[attr.key] = val.stringValue;
|
|
12
|
+
else if (val.intValue !== undefined)
|
|
13
|
+
result[attr.key] = parseInt(val.intValue);
|
|
14
|
+
else if (val.doubleValue !== undefined)
|
|
15
|
+
result[attr.key] = val.doubleValue;
|
|
16
|
+
else if (val.boolValue !== undefined)
|
|
17
|
+
result[attr.key] = val.boolValue;
|
|
18
|
+
else if (val.arrayValue !== undefined)
|
|
19
|
+
result[attr.key] = val.arrayValue; // Simplified
|
|
20
|
+
else if (val.kvlistValue !== undefined)
|
|
21
|
+
result[attr.key] = val.kvlistValue; // Simplified
|
|
22
|
+
else
|
|
23
|
+
result[attr.key] = val;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
export async function ingestCustomLogs(body, token, db, auth) {
|
|
29
|
+
const payload = await auth.verifyToken(token);
|
|
30
|
+
if (!payload || !payload.streamId) {
|
|
31
|
+
throw new Error("Unauthorized: Invalid token");
|
|
32
|
+
}
|
|
33
|
+
const { streamId, logs } = body;
|
|
34
|
+
console.log(`[API] Ingesting logs for stream: ${streamId}`);
|
|
35
|
+
if (streamId !== payload.streamId) {
|
|
36
|
+
throw new Error("Forbidden: Token does not match streamId");
|
|
37
|
+
}
|
|
38
|
+
if (!logs) {
|
|
39
|
+
throw new Error("Missing logs");
|
|
40
|
+
}
|
|
41
|
+
const logEntries = Array.isArray(logs) ? logs : [logs];
|
|
42
|
+
console.log(`[API] Processing ${logEntries.length} log entries`);
|
|
43
|
+
for (const log of logEntries) {
|
|
44
|
+
let content = "";
|
|
45
|
+
let metadata = {};
|
|
46
|
+
if (typeof log === "string") {
|
|
47
|
+
content = log;
|
|
48
|
+
}
|
|
49
|
+
else if (typeof log === "object") {
|
|
50
|
+
content = log.content || JSON.stringify(log);
|
|
51
|
+
metadata = log.metadata || {};
|
|
52
|
+
}
|
|
53
|
+
if (content) {
|
|
54
|
+
await db.addLog(streamId, content, metadata);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
console.log(`[API] /api/ingest Successfully added ${logEntries.length} logs`);
|
|
58
|
+
return { success: true, count: logEntries.length };
|
|
59
|
+
}
|
|
60
|
+
export async function ingestOtlpLogs(body, token, db, auth) {
|
|
61
|
+
const payload = await auth.verifyToken(token);
|
|
62
|
+
if (!payload || !payload.streamId) {
|
|
63
|
+
throw new Error("Unauthorized: Invalid token");
|
|
64
|
+
}
|
|
65
|
+
const streamId = payload.streamId;
|
|
66
|
+
console.log(`[API] Ingesting OTLP logs for stream: ${streamId}`);
|
|
67
|
+
const { resourceLogs } = body;
|
|
68
|
+
if (!resourceLogs || !Array.isArray(resourceLogs)) {
|
|
69
|
+
throw new Error("Invalid payload");
|
|
70
|
+
}
|
|
71
|
+
let count = 0;
|
|
72
|
+
for (const resourceLog of resourceLogs) {
|
|
73
|
+
const resourceAttrs = parseOtlpAttributes(resourceLog.resource?.attributes);
|
|
74
|
+
if (resourceLog.scopeLogs) {
|
|
75
|
+
for (const scopeLog of resourceLog.scopeLogs) {
|
|
76
|
+
const scopeName = scopeLog.scope?.name;
|
|
77
|
+
if (scopeLog.logRecords) {
|
|
78
|
+
for (const log of scopeLog.logRecords) {
|
|
79
|
+
let content = "";
|
|
80
|
+
if (log.body?.stringValue)
|
|
81
|
+
content = log.body.stringValue;
|
|
82
|
+
else if (log.body?.kvlistValue)
|
|
83
|
+
content = JSON.stringify(log.body.kvlistValue);
|
|
84
|
+
else if (typeof log.body === "string")
|
|
85
|
+
content = log.body; // Fallback
|
|
86
|
+
const logAttrs = parseOtlpAttributes(log.attributes);
|
|
87
|
+
// Merge attributes: Resource > Scope (if any) > Log
|
|
88
|
+
const metadata = {
|
|
89
|
+
...resourceAttrs,
|
|
90
|
+
...logAttrs,
|
|
91
|
+
severity: log.severityText || log.severityNumber,
|
|
92
|
+
scope: scopeName,
|
|
93
|
+
timestamp: log.timeUnixNano,
|
|
94
|
+
};
|
|
95
|
+
if (content) {
|
|
96
|
+
await db.addLog(streamId, content, metadata);
|
|
97
|
+
count++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
console.log(`[API] /v1/logs Ingested ${count} logs`);
|
|
105
|
+
return { partialSuccess: {}, logsIngested: count };
|
|
106
|
+
}
|
package/dist/api/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "path";
|
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import chalk from "chalk";
|
|
7
|
+
import { ingestCustomLogs, ingestOtlpLogs } from "./controllers.js";
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = path.dirname(__filename);
|
|
9
10
|
export async function startApiServer(db, auth) {
|
|
@@ -26,35 +27,6 @@ export async function startApiServer(db, auth) {
|
|
|
26
27
|
console.warn(chalk.yellow("Frontend build not found. Run 'npm run build' in packages/core/frontend to build the UI."));
|
|
27
28
|
}
|
|
28
29
|
await auth.initialize();
|
|
29
|
-
// console.log(chalk.bold.green(`\nš» API server running on:`));
|
|
30
|
-
// console.log(chalk.green(`http://localhost:${port}`));
|
|
31
|
-
// Helper to parse OTLP attributes
|
|
32
|
-
const parseOtlpAttributes = (attributes) => {
|
|
33
|
-
if (!Array.isArray(attributes))
|
|
34
|
-
return {};
|
|
35
|
-
const result = {};
|
|
36
|
-
for (const attr of attributes) {
|
|
37
|
-
if (attr.key && attr.value) {
|
|
38
|
-
// Extract value based on type (stringValue, intValue, boolValue, etc.)
|
|
39
|
-
const val = attr.value;
|
|
40
|
-
if (val.stringValue !== undefined)
|
|
41
|
-
result[attr.key] = val.stringValue;
|
|
42
|
-
else if (val.intValue !== undefined)
|
|
43
|
-
result[attr.key] = parseInt(val.intValue);
|
|
44
|
-
else if (val.doubleValue !== undefined)
|
|
45
|
-
result[attr.key] = val.doubleValue;
|
|
46
|
-
else if (val.boolValue !== undefined)
|
|
47
|
-
result[attr.key] = val.boolValue;
|
|
48
|
-
else if (val.arrayValue !== undefined)
|
|
49
|
-
result[attr.key] = val.arrayValue; // Simplified
|
|
50
|
-
else if (val.kvlistValue !== undefined)
|
|
51
|
-
result[attr.key] = val.kvlistValue; // Simplified
|
|
52
|
-
else
|
|
53
|
-
result[attr.key] = val;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
};
|
|
58
30
|
app.post("/v1/logs", async (req, res) => {
|
|
59
31
|
console.log(`[API] POST /v1/logs received`);
|
|
60
32
|
try {
|
|
@@ -64,60 +36,17 @@ export async function startApiServer(db, auth) {
|
|
|
64
36
|
return res.status(401).json({ code: 16, message: "Unauthenticated" });
|
|
65
37
|
}
|
|
66
38
|
const token = authHeader.split(" ")[1];
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
console.warn("[API] /v1/logs Unauthorized: Invalid token");
|
|
70
|
-
return res.status(401).json({ code: 16, message: "Invalid token" });
|
|
71
|
-
}
|
|
72
|
-
const streamId = payload.streamId;
|
|
73
|
-
console.log(`[API] Ingesting OTLP logs for stream: ${streamId}`);
|
|
74
|
-
const { resourceLogs } = req.body;
|
|
75
|
-
if (!resourceLogs || !Array.isArray(resourceLogs)) {
|
|
76
|
-
console.warn("[API] /v1/logs Invalid payload");
|
|
77
|
-
return res.status(400).json({ code: 3, message: "Invalid payload" });
|
|
78
|
-
}
|
|
79
|
-
// ... existing logic ...
|
|
80
|
-
let count = 0;
|
|
81
|
-
// ... loop ...
|
|
82
|
-
// (I will keep the existing loop logic but add a log at the end)
|
|
83
|
-
/* ... existing loop code ... */
|
|
84
|
-
for (const resourceLog of resourceLogs) {
|
|
85
|
-
const resourceAttrs = parseOtlpAttributes(resourceLog.resource?.attributes);
|
|
86
|
-
if (resourceLog.scopeLogs) {
|
|
87
|
-
for (const scopeLog of resourceLog.scopeLogs) {
|
|
88
|
-
const scopeName = scopeLog.scope?.name;
|
|
89
|
-
if (scopeLog.logRecords) {
|
|
90
|
-
for (const log of scopeLog.logRecords) {
|
|
91
|
-
let content = "";
|
|
92
|
-
if (log.body?.stringValue)
|
|
93
|
-
content = log.body.stringValue;
|
|
94
|
-
else if (log.body?.kvlistValue)
|
|
95
|
-
content = JSON.stringify(log.body.kvlistValue);
|
|
96
|
-
else if (typeof log.body === "string")
|
|
97
|
-
content = log.body; // Fallback
|
|
98
|
-
const logAttrs = parseOtlpAttributes(log.attributes);
|
|
99
|
-
// Merge attributes: Resource > Scope (if any) > Log
|
|
100
|
-
const metadata = {
|
|
101
|
-
...resourceAttrs,
|
|
102
|
-
...logAttrs,
|
|
103
|
-
severity: log.severityText || log.severityNumber,
|
|
104
|
-
scope: scopeName,
|
|
105
|
-
timestamp: log.timeUnixNano,
|
|
106
|
-
};
|
|
107
|
-
if (content) {
|
|
108
|
-
await db.addLog(streamId, content, metadata);
|
|
109
|
-
count++;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
console.log(`[API] /v1/logs Ingested ${count} logs`);
|
|
117
|
-
res.json({ partialSuccess: {}, logsIngested: count });
|
|
39
|
+
const result = await ingestOtlpLogs(req.body, token, db, auth);
|
|
40
|
+
res.json(result);
|
|
118
41
|
}
|
|
119
42
|
catch (e) {
|
|
120
43
|
console.error("OTLP Ingest error:", e);
|
|
44
|
+
if (e.message.includes("Unauthorized")) {
|
|
45
|
+
return res.status(401).json({ code: 16, message: e.message });
|
|
46
|
+
}
|
|
47
|
+
if (e.message.includes("Invalid payload")) {
|
|
48
|
+
return res.status(400).json({ code: 3, message: e.message });
|
|
49
|
+
}
|
|
121
50
|
res.status(500).json({ code: 13, message: String(e) });
|
|
122
51
|
}
|
|
123
52
|
});
|
|
@@ -130,42 +59,20 @@ export async function startApiServer(db, auth) {
|
|
|
130
59
|
return res.status(401).send("Unauthorized: Missing token");
|
|
131
60
|
}
|
|
132
61
|
const token = authHeader.split(" ")[1];
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
console.warn("[API] /api/ingest Unauthorized: Invalid token");
|
|
136
|
-
return res.status(401).send("Unauthorized: Invalid token");
|
|
137
|
-
}
|
|
138
|
-
const { streamId, logs } = req.body;
|
|
139
|
-
console.log(`[API] Ingesting logs for stream: ${streamId}`);
|
|
140
|
-
if (streamId !== payload.streamId) {
|
|
141
|
-
console.warn(`[API] /api/ingest Forbidden: Token streamId ${payload.streamId} != body streamId ${streamId}`);
|
|
142
|
-
return res.status(403).send("Forbidden: Token does not match streamId");
|
|
143
|
-
}
|
|
144
|
-
if (!logs) {
|
|
145
|
-
console.warn("[API] /api/ingest Missing logs");
|
|
146
|
-
return res.status(400).send("Missing logs");
|
|
147
|
-
}
|
|
148
|
-
const logEntries = Array.isArray(logs) ? logs : [logs];
|
|
149
|
-
console.log(`[API] Processing ${logEntries.length} log entries`);
|
|
150
|
-
for (const log of logEntries) {
|
|
151
|
-
let content = "";
|
|
152
|
-
let metadata = {};
|
|
153
|
-
if (typeof log === "string") {
|
|
154
|
-
content = log;
|
|
155
|
-
}
|
|
156
|
-
else if (typeof log === "object") {
|
|
157
|
-
content = log.content || JSON.stringify(log);
|
|
158
|
-
metadata = log.metadata || {};
|
|
159
|
-
}
|
|
160
|
-
if (content) {
|
|
161
|
-
await db.addLog(streamId, content, metadata);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
console.log(`[API] /api/ingest Successfully added ${logEntries.length} logs`);
|
|
165
|
-
res.json({ success: true, count: logEntries.length });
|
|
62
|
+
const result = await ingestCustomLogs(req.body, token, db, auth);
|
|
63
|
+
res.json(result);
|
|
166
64
|
}
|
|
167
65
|
catch (e) {
|
|
168
66
|
console.error("Ingest error:", e);
|
|
67
|
+
if (e.message.includes("Unauthorized")) {
|
|
68
|
+
return res.status(401).send(e.message);
|
|
69
|
+
}
|
|
70
|
+
if (e.message.includes("Forbidden")) {
|
|
71
|
+
return res.status(403).send(e.message);
|
|
72
|
+
}
|
|
73
|
+
if (e.message.includes("Missing logs")) {
|
|
74
|
+
return res.status(400).send(e.message);
|
|
75
|
+
}
|
|
169
76
|
res.status(500).json({ error: String(e) });
|
|
170
77
|
}
|
|
171
78
|
});
|
|
@@ -207,6 +114,15 @@ export async function startApiServer(db, auth) {
|
|
|
207
114
|
await db.deleteProject(id);
|
|
208
115
|
res.json({ success: true });
|
|
209
116
|
});
|
|
117
|
+
app.patch("/api/projects/:id", async (req, res) => {
|
|
118
|
+
const { id } = req.params;
|
|
119
|
+
const { name } = req.body;
|
|
120
|
+
if (!name || !name.trim()) {
|
|
121
|
+
return res.status(400).send("Name required");
|
|
122
|
+
}
|
|
123
|
+
const project = await db.renameProject(id, name.trim());
|
|
124
|
+
res.json(project);
|
|
125
|
+
});
|
|
210
126
|
app.get("/api/streams", async (req, res) => {
|
|
211
127
|
const projectId = req.query.projectId;
|
|
212
128
|
if (projectId) {
|
|
@@ -222,6 +138,15 @@ export async function startApiServer(db, auth) {
|
|
|
222
138
|
await db.deleteStream(id);
|
|
223
139
|
res.json({ success: true });
|
|
224
140
|
});
|
|
141
|
+
app.patch("/api/streams/:id", async (req, res) => {
|
|
142
|
+
const { id } = req.params;
|
|
143
|
+
const { name } = req.body;
|
|
144
|
+
if (!name || !name.trim()) {
|
|
145
|
+
return res.status(400).send("Name required");
|
|
146
|
+
}
|
|
147
|
+
const stream = await db.renameStream(id, name.trim());
|
|
148
|
+
res.json(stream);
|
|
149
|
+
});
|
|
225
150
|
app.get("/api/streams/:id/token", async (req, res) => {
|
|
226
151
|
const { id } = req.params;
|
|
227
152
|
try {
|
|
@@ -300,6 +225,50 @@ export async function startApiServer(db, auth) {
|
|
|
300
225
|
}
|
|
301
226
|
res.json(logs);
|
|
302
227
|
});
|
|
228
|
+
app.get("/api/issues", async (req, res) => {
|
|
229
|
+
const projectId = req.query.projectId;
|
|
230
|
+
const status = req.query.status;
|
|
231
|
+
const limit = parseInt(req.query.limit || "50");
|
|
232
|
+
if (!projectId) {
|
|
233
|
+
return res.status(400).send("Missing projectId");
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const issues = await db.getIssues(projectId, status, limit);
|
|
237
|
+
res.json(issues);
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
console.error("Error fetching issues:", e);
|
|
241
|
+
res.status(500).send(String(e));
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
app.get("/api/issues/:id", async (req, res) => {
|
|
245
|
+
const { id } = req.params;
|
|
246
|
+
try {
|
|
247
|
+
const data = await db.getIssue(id);
|
|
248
|
+
if (!data)
|
|
249
|
+
return res.status(404).send("Issue not found");
|
|
250
|
+
res.json(data);
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
console.error("Error fetching issue details:", e);
|
|
254
|
+
res.status(500).send(String(e));
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
app.patch("/api/issues/:id", async (req, res) => {
|
|
258
|
+
const { id } = req.params;
|
|
259
|
+
const { status } = req.body;
|
|
260
|
+
if (status !== "open" && status !== "resolved") {
|
|
261
|
+
return res.status(400).send("Invalid status");
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
await db.updateIssueStatus(id, status);
|
|
265
|
+
res.json({ success: true });
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
console.error("Error updating issue:", e);
|
|
269
|
+
res.status(500).send(String(e));
|
|
270
|
+
}
|
|
271
|
+
});
|
|
303
272
|
// SPA fallback
|
|
304
273
|
app.get("*", (req, res) => {
|
|
305
274
|
if (req.path.startsWith("/api")) {
|