@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.
@@ -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
+ }
@@ -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 payload = await auth.verifyToken(token);
68
- if (!payload || !payload.streamId) {
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 payload = await auth.verifyToken(token);
134
- if (!payload || !payload.streamId) {
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")) {