@oisasoje/gloo 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/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # Gloo 🚀
2
+
3
+ A lightweight Node.js/Express middleware for tracing HTTP requests using spans and logs, with a local receiver + live dashboard for inspection.
4
+
5
+ Gloo helps you see where time is spent inside a request in real time.
6
+
7
+ ---
8
+
9
+ ## Features (v1)
10
+
11
+ * ⚡ **Express Middleware:** Dynamic request-scoped lifecycle tracing.
12
+ * ðŸŒģ **Nested Spans:** Wrap async/sync blocks to measure nested code paths.
13
+ * 🧠 **Scoped Context:** Lightweight request-scoped isolation via `AsyncLocalStorage`.
14
+ * 📊 **Live Telemetry:** Streams active spans and logs over WebSockets in real time.
15
+ * ðŸ–Ĩ **Developer Dashboard:** A sleek developer UI built specifically to inspect request bottlenecks.
16
+ * ðŸŠķ **Featherweight SDK:** Zero runtime third-party package dependencies.
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install gloo
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Setup & Usage
29
+
30
+ ### 1. Register the Middleware
31
+ Mount the Gloo middleware at the top of your Express app:
32
+
33
+ ```javascript
34
+ import express from "express";
35
+ import { gloo } from "gloo";
36
+
37
+ const app = express();
38
+
39
+ app.use(gloo());
40
+ ```
41
+
42
+ ### 2. Wrap Spans & Logs
43
+ Track synchronous and asynchronous operations using `span()`, and write trace-scoped logs using `log()` or `error()`:
44
+
45
+ ```javascript
46
+ import { span, log, error } from "gloo";
47
+
48
+ app.get("/users/:id", async (req, res) => {
49
+ await span("get-user", async () => {
50
+ try {
51
+ const response = await fetch(`https://api.example.com/user/${req.params.id}`);
52
+ const data = await response.json();
53
+
54
+ await span("log-user-data", async () => {
55
+ log("user fetched successfully");
56
+ });
57
+
58
+ } catch (err) {
59
+ error(err);
60
+ }
61
+ });
62
+
63
+ res.json({ status: "ok" });
64
+ });
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Mental Model
70
+
71
+ Gloo tracks each incoming request as a tree of nested spans and logs:
72
+
73
+ ```text
74
+ HTTP Request
75
+ ├── middleware execution
76
+ ├── span: get-user
77
+ │ └── span: log-user-data (with log details)
78
+ └── response
79
+ ```
80
+
81
+ Each span records:
82
+ * Start time & end time
83
+ * Total duration (latency)
84
+ * Success/error state
85
+
86
+ ---
87
+
88
+ ## Start the Receiver
89
+
90
+ Before running your application, start the local telemetry receiver. It acts as the collector database and WebSocket broadcaster:
91
+
92
+ ```bash
93
+ npx gloo-receiver
94
+ ```
95
+
96
+ * **Collector API:** Runs on `http://localhost:7777`
97
+ * **Dashboard stream:** Broadcasts live trace payloads via WebSockets.
98
+
99
+ ---
100
+
101
+ ## Run the Dashboard
102
+
103
+ To view your traces in real-time, launch the visualizer dashboard app:
104
+
105
+ ```bash
106
+ npm run dev
107
+ ```
108
+
109
+ Then open your browser to:
110
+ ```text
111
+ http://localhost:3500
112
+ ```
113
+
114
+ The dashboard automatically hooks up to the receiver and streams live traces.
115
+
116
+ ---
117
+
118
+ ## Architecture (v1)
119
+
120
+ ```text
121
+ [ Express App ]
122
+ ↓
123
+ Gloo SDK
124
+ ↓
125
+ localhost:7777 (Receiver)
126
+ ↓
127
+ WebSocket Stream
128
+ ↓
129
+ Gloo Dashboard (UI on port 3500)
130
+ ```
131
+
132
+ ---
133
+
134
+ ## API Reference
135
+
136
+ ### `gloo()`
137
+ Express middleware that initializes request-scoped tracing context.
138
+ ```javascript
139
+ app.use(gloo());
140
+ ```
141
+
142
+ ### `span(name, fn)`
143
+ Wraps a block of code and measures execution time. Supports nested calls.
144
+ ```javascript
145
+ await span("db.query", async () => {
146
+ return db.user.findMany();
147
+ });
148
+ ```
149
+
150
+ ### `log(value)`
151
+ Attaches a log entry to the active trace scope.
152
+ ```javascript
153
+ log("fetching user data");
154
+ ```
155
+
156
+ ### `error(err)`
157
+ Records an error instance in the active trace timeline.
158
+ ```javascript
159
+ error(err);
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Notes
165
+ * Gloo is custom-designed for local development observability.
166
+ * The telemetry receiver must be running for traces to be successfully captured.
167
+ * The dashboard reads from the receiver server, remaining completely decoupled from your application code.
@@ -0,0 +1,5 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ const als = new AsyncLocalStorage();
4
+
5
+ export default als;
package/endSpan.js ADDED
@@ -0,0 +1,13 @@
1
+ import als from "./asyncLocalStorage.js";
2
+
3
+ const endSpan = (span) => {
4
+ const trace = als.getStore();
5
+
6
+ if (!trace) return;
7
+
8
+ const now = Date.now();
9
+ span.endTime = now;
10
+ span.latency = now - span.startTime;
11
+ };
12
+
13
+ export default endSpan;
package/error.js ADDED
@@ -0,0 +1,14 @@
1
+ import als from "./asyncLocalStorage.js";
2
+
3
+ const error = (msg) => {
4
+ const trace = als.getStore();
5
+
6
+ if (!trace) return;
7
+
8
+ const errorTag = msg instanceof Error ? msg.message : String(msg);
9
+
10
+ const now = Date.now();
11
+ trace.errors.push({ tag: errorTag, startTime: now, latency: 0 });
12
+ };
13
+
14
+ export default error;
package/gloo.js ADDED
@@ -0,0 +1,35 @@
1
+ import als from "./asyncLocalStorage.js";
2
+ import initTrace from "./initTrace.js";
3
+ import { sendTrace } from "./sendTrace.js";
4
+
5
+ const gloo = () => {
6
+ return (req, res, next) => {
7
+ const { receiverId, traceData } = initTrace(req, res);
8
+
9
+ let finished = false;
10
+ const finalizeTrace = (status) => {
11
+ if (finished) return;
12
+ finished = true;
13
+
14
+ const latency = Date.now() - traceData.startTime;
15
+ traceData.latency = latency;
16
+ traceData.status = status || res.statusCode;
17
+ traceData.responseTime = `${latency}ms`;
18
+
19
+ sendTrace(traceData);
20
+ };
21
+
22
+ als.run(traceData, () => {
23
+ res.on("finish", () => finalizeTrace(res.statusCode));
24
+ res.on("close", () => {
25
+ if (!res.writableEnded) {
26
+ finalizeTrace(499); // 499 Client Closed Request
27
+ }
28
+ });
29
+
30
+ next();
31
+ });
32
+ };
33
+ };
34
+
35
+ export default gloo;
package/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import gloo from "./gloo.js";
2
+ import span from "./span.js";
3
+ import log from "./log.js";
4
+ import error from "./error.js";
5
+
6
+ export { gloo, span, log, error };
package/initTrace.js ADDED
@@ -0,0 +1,21 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const initTrace = (req, res) => {
4
+ const receiverId = crypto.randomUUID();
5
+ req.glooId = receiverId;
6
+
7
+ const traceData = {
8
+ id: receiverId,
9
+ method: req.method,
10
+ url: req.originalUrl,
11
+ status: res.statusCode,
12
+ startTime: Date.now(),
13
+ logs: [],
14
+ spans: [],
15
+ errors: [],
16
+ };
17
+
18
+ return { receiverId, traceData };
19
+ };
20
+
21
+ export default initTrace;
package/log.js ADDED
@@ -0,0 +1,12 @@
1
+ import als from "./asyncLocalStorage.js";
2
+
3
+ const log = (msg) => {
4
+ const trace = als.getStore();
5
+
6
+ if (!trace) return;
7
+
8
+ const now = Date.now();
9
+ trace.logs.push({ tag: msg, startTime: now, latency: 0 });
10
+ };
11
+
12
+ export default log;
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@oisasoje/gloo",
3
+ "version": "1.0.0",
4
+ "description": "Premium real-time request tracing, nested span tracking, and error logging SDK for Node.js Express.",
5
+ "license": "ISC",
6
+ "author": "Victor",
7
+ "type": "module",
8
+ "main": "index.js",
9
+ "exports": {
10
+ ".": "./index.js"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "gloo.js",
15
+ "asyncLocalStorage.js",
16
+ "initTrace.js",
17
+ "sendTrace.js",
18
+ "startSpan.js",
19
+ "endSpan.js",
20
+ "span.js",
21
+ "log.js",
22
+ "error.js"
23
+ ],
24
+ "dependencies": {}
25
+ }
package/sendTrace.js ADDED
@@ -0,0 +1,15 @@
1
+ export const sendTrace = async (event) => {
2
+ try {
3
+ const response = await fetch("http://localhost:7777/gloo/events", {
4
+ method: "POST",
5
+ headers: { "Content-Type": "application/json" },
6
+ body: JSON.stringify(event),
7
+ });
8
+
9
+ if (!response.ok) {
10
+ throw new Error(`Failed to send trace: ${response.status}`);
11
+ }
12
+ } catch (error) {
13
+ console.error("Gloo transport error:", error.message);
14
+ }
15
+ };
package/span.js ADDED
@@ -0,0 +1,30 @@
1
+ import startSpan from "./startSpan.js";
2
+ import endSpan from "./endSpan.js";
3
+ import als from "./asyncLocalStorage.js";
4
+
5
+ const span = async (name, fn) => {
6
+ const trace = als.getStore();
7
+
8
+ if (!trace) {
9
+ try {
10
+ return await fn();
11
+ } catch (err) {
12
+ throw err;
13
+ }
14
+ }
15
+ const s = startSpan(name);
16
+
17
+ try {
18
+ const result = await fn();
19
+ s.status = "ok";
20
+ return result;
21
+ } catch (err) {
22
+ s.status = "error";
23
+ s.error = err.message || String(err);
24
+ throw err;
25
+ } finally {
26
+ endSpan(s);
27
+ }
28
+ };
29
+
30
+ export default span;
package/startSpan.js ADDED
@@ -0,0 +1,13 @@
1
+ import als from "./asyncLocalStorage.js";
2
+
3
+ const startSpan = (name) => {
4
+ const trace = als.getStore();
5
+
6
+ if (!trace) return;
7
+
8
+ const span = { tag: name, startTime: Date.now() };
9
+ trace.spans.push(span);
10
+ return span;
11
+ };
12
+
13
+ export default startSpan;