@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 +167 -0
- package/asyncLocalStorage.js +5 -0
- package/endSpan.js +13 -0
- package/error.js +14 -0
- package/gloo.js +35 -0
- package/index.js +6 -0
- package/initTrace.js +21 -0
- package/log.js +12 -0
- package/package.json +25 -0
- package/sendTrace.js +15 -0
- package/span.js +30 -0
- package/startSpan.js +13 -0
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.
|
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
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
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;
|