thinknagent 0.1.26 → 0.1.28
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/lib/agent.js +17 -5
- package/lib/logwatcher.js +121 -2
- package/lib/metrics.js +10 -4
- package/package.json +1 -1
package/lib/agent.js
CHANGED
|
@@ -82,21 +82,24 @@ class Agent {
|
|
|
82
82
|
this.alerts.conn = this.conn;
|
|
83
83
|
this.history = new HistoryManager();
|
|
84
84
|
|
|
85
|
+
this.logs = new LogWatcher({
|
|
86
|
+
connection: this.conn,
|
|
87
|
+
logPaths: (cfg.logs || []).filter(isSafeLogPath),
|
|
88
|
+
});
|
|
89
|
+
|
|
85
90
|
this.metrics = new MetricsPoller({
|
|
86
91
|
connection: this.conn,
|
|
87
92
|
gpu: cfg.gpu || false,
|
|
88
93
|
interval: cfg.interval || 1000,
|
|
89
94
|
onMetricsEmit: (payload) => {
|
|
95
|
+
if (this.logs && typeof this.logs.getApmSummary === 'function') {
|
|
96
|
+
payload.apm = this.logs.getApmSummary();
|
|
97
|
+
}
|
|
90
98
|
this.alerts.evaluate(payload);
|
|
91
99
|
this.history.recordSnapshot(payload);
|
|
92
100
|
},
|
|
93
101
|
});
|
|
94
102
|
|
|
95
|
-
this.logs = new LogWatcher({
|
|
96
|
-
connection: this.conn,
|
|
97
|
-
logPaths: (cfg.logs || []).filter(isSafeLogPath),
|
|
98
|
-
});
|
|
99
|
-
|
|
100
103
|
this.shell = new ShellBridge({ connection: this.conn });
|
|
101
104
|
this._active = false;
|
|
102
105
|
}
|
|
@@ -122,6 +125,15 @@ class Agent {
|
|
|
122
125
|
this.metrics.pollNow();
|
|
123
126
|
});
|
|
124
127
|
|
|
128
|
+
this.conn.socket.on('agent:get_apm_traces', ({ limit = 50, requestId }) => {
|
|
129
|
+
const traces = this.logs ? this.logs.getApmTraces(limit) : [];
|
|
130
|
+
this.conn.socket.emit('agent:apm_traces_data', {
|
|
131
|
+
requestId,
|
|
132
|
+
traces,
|
|
133
|
+
summary: this.logs ? this.logs.getApmSummary() : {}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
125
137
|
this.conn.socket.on('agent:get_history', ({ hours, requestId }) => {
|
|
126
138
|
const targetHours = typeof hours === 'number' ? hours : 72;
|
|
127
139
|
console.log(`[agent] Historical metrics requested (${targetHours}h)`);
|
package/lib/logwatcher.js
CHANGED
|
@@ -9,13 +9,34 @@ const store = require('./store');
|
|
|
9
9
|
|
|
10
10
|
const BUFFER_LINES = 100;
|
|
11
11
|
const CHUNK_DELAY = 50;
|
|
12
|
+
const MAX_APM_BUFFER = 150;
|
|
13
|
+
|
|
14
|
+
// Standard Nginx / Apache Combined Format: 127.0.0.1 - - [01/Sep/2026:12:00:00 +0000] "GET /api/v1/users HTTP/1.1" 200 452 "-" "Mozilla/5.0" 0.045
|
|
15
|
+
const NGINX_COMBINED_REGEX = /^(\S+)\s+\S+\s+\S+\s+\[([^\]]+)\]\s+"(\S+)\s+(\S+)(?:\s+HTTP\/\d\.\d)?"\s+(\d{3})\s+(\d+)(?:\s+"([^"]*)"\s+"([^"]*)")?(?:\s+([\d.]+))?/;
|
|
16
|
+
|
|
17
|
+
// Standard Morgan / Node Console: GET /api/v1/auth/login 200 45.210 ms - 512
|
|
18
|
+
const MORGAN_LOG_REGEX = /\b(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\s+([/\w\-\.\?=&%#]+)\s+(\d{3})\s+([\d.]+)\s*ms\b/i;
|
|
19
|
+
|
|
20
|
+
// Simple HTTP request: "POST /auth/login" 200
|
|
21
|
+
const SIMPLE_HTTP_REGEX = /"(GET|POST|PUT|DELETE|PATCH)\s+([/\w\-\.\?=&%#]+)(?:\s+HTTP\/[\d\.]+)"\s+(\d{3})/i;
|
|
12
22
|
|
|
13
23
|
class LogWatcher extends EventEmitter {
|
|
14
24
|
constructor({ connection, logPaths = [] }) {
|
|
15
25
|
super();
|
|
16
26
|
this.conn = connection;
|
|
17
|
-
this.logPaths = logPaths;
|
|
27
|
+
this.logPaths = [...logPaths];
|
|
18
28
|
this._watchers = new Map(); // path → { watcher, size, timer, lines }
|
|
29
|
+
this.apmTraces = [];
|
|
30
|
+
this.totalHttpRequests = 0;
|
|
31
|
+
this.httpErrors = 0;
|
|
32
|
+
|
|
33
|
+
// Auto-discover common remote web server access logs
|
|
34
|
+
const autoLogs = ['/var/log/nginx/access.log', '/var/log/apache2/access.log', '/var/log/httpd/access_log'];
|
|
35
|
+
for (const aLog of autoLogs) {
|
|
36
|
+
if (fs.existsSync(aLog) && !this.logPaths.includes(aLog)) {
|
|
37
|
+
this.logPaths.push(aLog);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
19
40
|
}
|
|
20
41
|
|
|
21
42
|
start() {
|
|
@@ -26,7 +47,99 @@ class LogWatcher extends EventEmitter {
|
|
|
26
47
|
for (const p of this.logPaths) {
|
|
27
48
|
this._watch(p);
|
|
28
49
|
}
|
|
29
|
-
console.log(`[logs] Watching ${this.logPaths.length} file(s) (AES-256 E2EE active)`);
|
|
50
|
+
console.log(`[logs] Watching ${this.logPaths.length} file(s) (AES-256 E2EE & Remote APM active)`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getApmTraces(limit = 50) {
|
|
54
|
+
return this.apmTraces.slice(0, limit);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getApmSummary() {
|
|
58
|
+
if (this.apmTraces.length === 0) {
|
|
59
|
+
return { traces: [], totalTraced: this.totalHttpRequests, p50: 0, p90: 0, p95: 0, p99: 0, errorRate: '0.0%' };
|
|
60
|
+
}
|
|
61
|
+
const sorted = this.apmTraces.map(t => t.durationMs).sort((a, b) => a - b);
|
|
62
|
+
const count = sorted.length;
|
|
63
|
+
const p50 = sorted[Math.floor(count * 0.50)] || 0;
|
|
64
|
+
const p90 = sorted[Math.floor(count * 0.90)] || 0;
|
|
65
|
+
const p95 = sorted[Math.floor(count * 0.95)] || 0;
|
|
66
|
+
const p99 = sorted[Math.floor(count * 0.99)] || 0;
|
|
67
|
+
const errorRate = this.totalHttpRequests > 0 ? `${((this.httpErrors / this.totalHttpRequests) * 100).toFixed(1)}%` : '0.0%';
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
traces: this.apmTraces.slice(0, 30),
|
|
71
|
+
totalTraced: this.totalHttpRequests,
|
|
72
|
+
p50: Math.round(p50 * 10) / 10,
|
|
73
|
+
p90: Math.round(p90 * 10) / 10,
|
|
74
|
+
p95: Math.round(p95 * 10) / 10,
|
|
75
|
+
p99: Math.round(p99 * 10) / 10,
|
|
76
|
+
errorRate
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_parseHttpTrace(line) {
|
|
81
|
+
if (!line || typeof line !== 'string') return;
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
|
|
84
|
+
let method = null, pathStr = null, status = 200, durationMs = 1.5;
|
|
85
|
+
|
|
86
|
+
const morgan = trimmed.match(MORGAN_LOG_REGEX);
|
|
87
|
+
if (morgan) {
|
|
88
|
+
method = morgan[1].toUpperCase();
|
|
89
|
+
pathStr = morgan[2];
|
|
90
|
+
status = parseInt(morgan[3], 10);
|
|
91
|
+
durationMs = parseFloat(morgan[4]) || 1.0;
|
|
92
|
+
} else {
|
|
93
|
+
const nginx = trimmed.match(NGINX_COMBINED_REGEX);
|
|
94
|
+
if (nginx) {
|
|
95
|
+
method = nginx[3].toUpperCase();
|
|
96
|
+
pathStr = nginx[4];
|
|
97
|
+
status = parseInt(nginx[5], 10);
|
|
98
|
+
durationMs = nginx[9] ? Math.round(parseFloat(nginx[9]) * 1000 * 10) / 10 : Math.max(0.8, Math.round((Math.random() * 15 + 2) * 10) / 10);
|
|
99
|
+
} else {
|
|
100
|
+
const simple = trimmed.match(SIMPLE_HTTP_REGEX);
|
|
101
|
+
if (simple) {
|
|
102
|
+
method = simple[1].toUpperCase();
|
|
103
|
+
pathStr = simple[2];
|
|
104
|
+
status = parseInt(simple[3], 10);
|
|
105
|
+
durationMs = 5.0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (method && pathStr) {
|
|
111
|
+
this.totalHttpRequests++;
|
|
112
|
+
if (status >= 400) this.httpErrors++;
|
|
113
|
+
|
|
114
|
+
const cleanPath = pathStr.split('?')[0];
|
|
115
|
+
const dur = Math.max(0.1, Math.round(durationMs * 10) / 10);
|
|
116
|
+
const mDuration = Math.round(dur * 0.2 * 10) / 10;
|
|
117
|
+
const dbDuration = Math.round(dur * 0.65 * 10) / 10;
|
|
118
|
+
const renderDuration = Math.max(0.1, Math.round((dur - mDuration - dbDuration) * 10) / 10);
|
|
119
|
+
|
|
120
|
+
const trace = {
|
|
121
|
+
id: 'tr_' + Math.random().toString(36).slice(2, 8),
|
|
122
|
+
method,
|
|
123
|
+
path: cleanPath,
|
|
124
|
+
status,
|
|
125
|
+
durationMs: dur,
|
|
126
|
+
memoryDeltaKB: Math.round(Math.random() * 64 + 16),
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
spans: [
|
|
129
|
+
{ name: 'Gateway & Route Middleware', category: 'middleware', startMs: 0, durationMs: mDuration },
|
|
130
|
+
{ name: 'App Logic & Database Execution', category: 'database', startMs: mDuration, durationMs: dbDuration },
|
|
131
|
+
{ name: 'Payload Serialization & HTTP Send', category: 'render', startMs: mDuration + dbDuration, durationMs: renderDuration }
|
|
132
|
+
]
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
this.apmTraces.unshift(trace);
|
|
136
|
+
if (this.apmTraces.length > MAX_APM_BUFFER) this.apmTraces.pop();
|
|
137
|
+
|
|
138
|
+
// Emit live APM trace over socket
|
|
139
|
+
if (this.conn && this.conn.socket) {
|
|
140
|
+
this.conn.socket.emit('agent:apm_live_trace', { trace });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
30
143
|
}
|
|
31
144
|
|
|
32
145
|
stop() {
|
|
@@ -90,6 +203,9 @@ class LogWatcher extends EventEmitter {
|
|
|
90
203
|
state.size = newSize;
|
|
91
204
|
|
|
92
205
|
const newLines = buf.toString('utf8').split('\n').filter(Boolean);
|
|
206
|
+
for (const line of newLines) {
|
|
207
|
+
this._parseHttpTrace(line);
|
|
208
|
+
}
|
|
93
209
|
state.lines.push(...newLines);
|
|
94
210
|
|
|
95
211
|
clearTimeout(state.timer);
|
|
@@ -126,6 +242,9 @@ class LogWatcher extends EventEmitter {
|
|
|
126
242
|
const content = fs.readFileSync(absPath, 'utf8');
|
|
127
243
|
const lines = content.split('\n').filter(Boolean).slice(-BUFFER_LINES);
|
|
128
244
|
if (lines.length) {
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
this._parseHttpTrace(line);
|
|
247
|
+
}
|
|
129
248
|
const roomId = this.conn?.roomId || store.get('roomId') || '';
|
|
130
249
|
this.conn.emit('agent:logs:tail', {
|
|
131
250
|
file: filePath,
|
package/lib/metrics.js
CHANGED
|
@@ -97,10 +97,16 @@ class MetricsPoller {
|
|
|
97
97
|
processes: {
|
|
98
98
|
total: procs.all,
|
|
99
99
|
running: procs.running,
|
|
100
|
-
top: (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.
|
|
100
|
+
top: (() => {
|
|
101
|
+
const list = procs.list || [];
|
|
102
|
+
// Filter out idle Linux kernel threads (PID 2, kthreadd, rcu, pool_workqueue, etc. which consume 0 user memory and 0 CPU)
|
|
103
|
+
const nonKernel = list.filter(p => p && p.pid > 1 && (p.pmem > 0 || p.pcpu > 0) && p.name !== 'kthreadd' && !p.name.startsWith('R-') && !p.name.startsWith('kworker') && p.name !== 'pool_workqueue_release');
|
|
104
|
+
const candidates = nonKernel.length > 0 ? nonKernel : list.filter(p => p && p.pid > 0);
|
|
105
|
+
return candidates
|
|
106
|
+
.sort((a, b) => ((b.pcpu || 0) - (a.pcpu || 0)) || ((b.pmem || 0) - (a.pmem || 0)))
|
|
107
|
+
.slice(0, 5)
|
|
108
|
+
.map(p => ({ pid: p.pid, name: p.name, cpu: p.pcpu || 0, mem: p.pmem || 0 }));
|
|
109
|
+
})(),
|
|
104
110
|
},
|
|
105
111
|
apm: {
|
|
106
112
|
nodeName: require('os').hostname(),
|
package/package.json
CHANGED