whatap 1.0.14 → 2.0.0-canary.1
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/.claude/settings.local.json +8 -1
- package/agent/darwin/arm64/whatap_nodejs +0 -0
- package/agent/linux/amd64/whatap_nodejs +0 -0
- package/agent/linux/arm64/whatap_nodejs +0 -0
- package/build.txt +2 -2
- package/lib/conf/config-default.js +6 -1
- package/lib/core/agent.js +110 -2
- package/lib/counter/counter-manager.js +10 -1
- package/lib/counter/task/llm-stat-tasks.js +268 -0
- package/lib/io/data-outputx.js +1 -1
- package/lib/llm/llm-id-util.js +35 -0
- package/lib/llm/llm-log-sink-pack.js +273 -0
- package/lib/llm/llm-pcode.js +33 -0
- package/lib/llm/llm-sender.js +475 -0
- package/lib/llm/llm-stat-collector.js +331 -0
- package/lib/observers/cron-observer.js +2 -2
- package/lib/observers/global-observer.js +57 -1
- package/lib/observers/grpc-observer.js +4 -2
- package/lib/observers/http-observer.js +85 -15
- package/lib/observers/ioredis-observer.js +1 -1
- package/lib/observers/kafka-observer.js +2 -2
- package/lib/observers/openai-observer.js +769 -0
- package/lib/observers/redis-observer.js +0 -1
- package/lib/observers/socket.io-observer.js +3 -2
- package/lib/observers/websocket-observer.js +3 -2
- package/lib/trace/trace-context.js +4 -0
- package/lib/udp/async_sender.js +20 -1
- package/lib/udp/packet_enum.js +1 -1
- package/lib/udp/udp_session.js +130 -5
- package/lib/util/keygen.js +3 -0
- package/package.json +2 -2
- package/whatap-20260208.log +15983 -0
- package/whatap-20260209.log +52 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(grep -n \"ECONNREFUSED\" /Users/seunghunlee/agent_workspace/nodejs_agent/lib/observers/*.js /Users/seunghunlee/agent_workspace/nodejs_agent/lib/core/agent.js 2>/dev/null)"
|
|
4
|
+
"Bash(grep -n \"ECONNREFUSED\" /Users/seunghunlee/agent_workspace/nodejs_agent/lib/observers/*.js /Users/seunghunlee/agent_workspace/nodejs_agent/lib/core/agent.js 2>/dev/null)",
|
|
5
|
+
"WebSearch",
|
|
6
|
+
"Bash(go env *)",
|
|
7
|
+
"Bash(ls -l \"$\\(go env GOPATH\\)/bin/govulncheck\")",
|
|
8
|
+
"Bash(brew list *)",
|
|
9
|
+
"Bash(brew search *)",
|
|
10
|
+
"Bash(brew info *)",
|
|
11
|
+
"Bash(echo \"--- exit $? ---\")"
|
|
5
12
|
]
|
|
6
13
|
}
|
|
7
14
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/build.txt
CHANGED
|
@@ -285,7 +285,12 @@ var ConfigDefault = {
|
|
|
285
285
|
"trace_kafka_enabled": bool("trace_kafka_enabled", false),
|
|
286
286
|
"trace_kafka_consumer_enabled": bool("trace_kafka_consumer_enabled", false),
|
|
287
287
|
|
|
288
|
-
"agent_per_instance_enabled": bool("agent_per_instance_enabled", false)
|
|
288
|
+
"agent_per_instance_enabled": bool("agent_per_instance_enabled", false),
|
|
289
|
+
|
|
290
|
+
// LLM monitoring
|
|
291
|
+
"llm_enabled": bool("llm_enabled", false),
|
|
292
|
+
"llm_net_udp_port": num("llm_net_udp_port", 0),
|
|
293
|
+
"llm_model_pricing": str("llm_model_pricing", "")
|
|
289
294
|
|
|
290
295
|
};
|
|
291
296
|
|
package/lib/core/agent.js
CHANGED
|
@@ -37,7 +37,8 @@ var Interceptor = require('./interceptor').Interceptor,
|
|
|
37
37
|
OracleObserver = require('../observers/oracle-observer').OracleObserver,
|
|
38
38
|
CustomMethodObserver = require('../observers/custom-method-observer').CustomMethodObserver,
|
|
39
39
|
CronObserver = require('../observers/cron-observer').CronObserver,
|
|
40
|
-
KafkaObserver = require('../observers/kafka-observer').KafkaObserver
|
|
40
|
+
KafkaObserver = require('../observers/kafka-observer').KafkaObserver,
|
|
41
|
+
OpenAIObserver = require('../observers/openai-observer').OpenAIObserver;
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
var Configuration = require('./../conf/configure'),
|
|
@@ -996,7 +997,7 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
|
|
|
996
997
|
if (pid) {
|
|
997
998
|
try {
|
|
998
999
|
if (this.isGoAgentRunning(pid)) {
|
|
999
|
-
// Windows: 기존 프로세스가 실행 중이면 스킵
|
|
1000
|
+
// Windows: 기존 프로세스가 실행 중이면 스킵
|
|
1000
1001
|
// Unix/Linux: 기존 프로세스 종료 후 새로 시작
|
|
1001
1002
|
if (process.platform === 'win32') {
|
|
1002
1003
|
Logger.print("WHATAP-106", `Agent already running (PID: ${pid}). Skipping duplicate execution.`, false);
|
|
@@ -1249,6 +1250,106 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
|
|
|
1249
1250
|
}
|
|
1250
1251
|
};
|
|
1251
1252
|
|
|
1253
|
+
/**
|
|
1254
|
+
* Start a separate Go Agent for LLM monitoring (with --llm flag)
|
|
1255
|
+
*/
|
|
1256
|
+
NodeAgent.prototype.startLlmGoAgent = function () {
|
|
1257
|
+
var self = this;
|
|
1258
|
+
var whatapHome = process.env.WHATAP_HOME;
|
|
1259
|
+
if (!whatapHome) return;
|
|
1260
|
+
|
|
1261
|
+
try {
|
|
1262
|
+
var platform = process.platform;
|
|
1263
|
+
var agentBinaryName = platform === 'win32' ? AGENT_NAME + '.exe' : AGENT_NAME;
|
|
1264
|
+
var agentPath = path.join(whatapHome, agentBinaryName);
|
|
1265
|
+
|
|
1266
|
+
if (!fs.existsSync(agentPath)) {
|
|
1267
|
+
Logger.print("WHATAP-LLM-020", "Agent binary not found for LLM agent: " + agentPath, false);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// LLM UDP port
|
|
1272
|
+
var basePort = parseInt(self._conf.getProperty('net_udp_port', 6600)) || 6600;
|
|
1273
|
+
var llmPort = parseInt(self._conf.getProperty('llm_net_udp_port', 0)) || (basePort + 100);
|
|
1274
|
+
self._conf['llm_net_udp_port'] = llmPort;
|
|
1275
|
+
|
|
1276
|
+
// Write to whatap.conf so Go Agent can read it
|
|
1277
|
+
try {
|
|
1278
|
+
var confFile = path.join(whatapHome, 'whatap.conf');
|
|
1279
|
+
if (fs.existsSync(confFile)) {
|
|
1280
|
+
var confContent = fs.readFileSync(confFile, 'utf8');
|
|
1281
|
+
if (confContent.indexOf('llm_net_udp_port') < 0) {
|
|
1282
|
+
fs.appendFileSync(confFile, '\nllm_net_udp_port=' + llmPort + '\n');
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
} catch (e) {
|
|
1286
|
+
Logger.printError("WHATAP-LLM-021", "Error writing llm_net_udp_port to whatap.conf", e, false);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Check existing LLM agent
|
|
1290
|
+
var llmPidFile = path.join(whatapHome, AGENT_NAME + '.pid.llm');
|
|
1291
|
+
if (fs.existsSync(llmPidFile)) {
|
|
1292
|
+
try {
|
|
1293
|
+
var existingPid = parseInt(fs.readFileSync(llmPidFile, 'utf8').trim());
|
|
1294
|
+
if (existingPid && self.isGoAgentRunning(existingPid)) {
|
|
1295
|
+
Logger.print("WHATAP-LLM-022", "LLM Go Agent already running (PID: " + existingPid + ")", false);
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
} catch (e) {}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Build environment
|
|
1302
|
+
var newEnv = Object.assign({}, process.env);
|
|
1303
|
+
newEnv['WHATAP_NET_UDP_PORT'] = String(llmPort);
|
|
1304
|
+
newEnv['WHATAP_PID_FILE'] = AGENT_NAME + '.pid.llm';
|
|
1305
|
+
newEnv['NODEJS_PARENT_APP_PID'] = process.pid.toString();
|
|
1306
|
+
newEnv['whatap.enabled'] = 'true';
|
|
1307
|
+
|
|
1308
|
+
// Spawn: whatap_nodejs -t 2 -d 1 --llm (normal agent와 동일 -t/-d, --llm만 추가)
|
|
1309
|
+
var spawnArgs = platform === 'win32'
|
|
1310
|
+
? ['-t', '2', '--llm', 'foreground']
|
|
1311
|
+
: ['-t', '2', '-d', '1', '--llm'];
|
|
1312
|
+
|
|
1313
|
+
var spawnOptions = {
|
|
1314
|
+
cwd: whatapHome,
|
|
1315
|
+
env: newEnv,
|
|
1316
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1317
|
+
detached: true, //
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
if (platform === 'win32') {
|
|
1321
|
+
spawnOptions.windowsHide = true;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
var llmProcess = child_process.spawn(agentPath, spawnArgs, spawnOptions);
|
|
1325
|
+
|
|
1326
|
+
llmProcess.on('spawn', function () {
|
|
1327
|
+
Logger.print("WHATAP-LLM-023", "executed LLM golang module in background (PID: " + llmProcess.pid + ")", false);
|
|
1328
|
+
try {
|
|
1329
|
+
fs.writeFileSync(llmPidFile, String(llmProcess.pid));
|
|
1330
|
+
} catch (e) {}
|
|
1331
|
+
Logger.print("WHATAP-LLM-028", "WHATAP: AGENT UP! (process name: " + AGENT_NAME + " --llm)", false);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
llmProcess.on('error', function (err) {
|
|
1335
|
+
Logger.printError("WHATAP-LLM-024", "Failed to spawn LLM Go Agent", err, false);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
llmProcess.on('close', function (code) {
|
|
1339
|
+
Logger.print("WHATAP-LLM-025", "LLM Go Agent exited with code: " + code, false);
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
// Capture stderr for logging
|
|
1343
|
+
llmProcess.stderr.on('data', function (data) {
|
|
1344
|
+
var msg = data.toString().trim();
|
|
1345
|
+
if (msg) Logger.print("WHATAP-LLM-026", "[LLM Agent] " + msg, false);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
Logger.printError("WHATAP-LLM-027", "Error starting LLM Go Agent", e, false);
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1252
1353
|
// 프로세스 종료 시 정리 작업 개선
|
|
1253
1354
|
process.on('exit', () => {
|
|
1254
1355
|
if (!process.env.WHATAP_HOME || !NodeAgent.prototype.getApplicationIdentifier || !NodeAgent.prototype.isPM2ClusterMode) {
|
|
@@ -1374,6 +1475,12 @@ NodeAgent.prototype.init = function(cb) {
|
|
|
1374
1475
|
// Start Node.js agent with proper port configuration
|
|
1375
1476
|
WhatapUtil.printWhatap();
|
|
1376
1477
|
self.startGoAgent();
|
|
1478
|
+
|
|
1479
|
+
// Start LLM Go Agent if llm_enabled
|
|
1480
|
+
if (self._conf.getProperty('llm_enabled', false)) {
|
|
1481
|
+
self.startLlmGoAgent();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1377
1484
|
TraceContextManager.initialized = true;
|
|
1378
1485
|
|
|
1379
1486
|
self.initUdp();
|
|
@@ -1413,6 +1520,7 @@ NodeAgent.prototype.loadObserves = function() {
|
|
|
1413
1520
|
observes.push(OracleObserver);
|
|
1414
1521
|
observes.push(CronObserver);
|
|
1415
1522
|
observes.push(KafkaObserver);
|
|
1523
|
+
observes.push(OpenAIObserver);
|
|
1416
1524
|
|
|
1417
1525
|
var packageToObserve = {};
|
|
1418
1526
|
observes.forEach(function(observeObj) {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
var conf = require('../conf/configure'),
|
|
8
8
|
Logger = require('../logger'),
|
|
9
|
-
GCAction = require('../system/gc-action')
|
|
9
|
+
GCAction = require('../system/gc-action'),
|
|
10
|
+
LlmStatTasks = require('./task/llm-stat-tasks');
|
|
10
11
|
|
|
11
12
|
function CounterManager(agent) {
|
|
12
13
|
this.agent = agent;
|
|
@@ -21,6 +22,14 @@ CounterManager.prototype.run = function () {
|
|
|
21
22
|
var tasks = [];
|
|
22
23
|
tasks.push(new GCAction());
|
|
23
24
|
|
|
25
|
+
// 호출 카운트는 ACTIVE_STATS 꼬리의 "LLM:count=N"으로 전송 (_llmMeterCount).
|
|
26
|
+
tasks.push(new LlmStatTasks.LLMActiveStatTask());
|
|
27
|
+
tasks.push(new LlmStatTasks.LLMApiStatusTask());
|
|
28
|
+
tasks.push(new LlmStatTasks.LLMErrorStatTask());
|
|
29
|
+
tasks.push(new LlmStatTasks.LLMFeatureStatTask());
|
|
30
|
+
tasks.push(new LlmStatTasks.LLMPerfStatTask());
|
|
31
|
+
tasks.push(new LlmStatTasks.LLMTokenUsageTask());
|
|
32
|
+
|
|
24
33
|
self.intervalIndex = setInterval(function(){
|
|
25
34
|
self.process(tasks);
|
|
26
35
|
},5000);
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LLM Metric counter tasks (7 categories):
|
|
5
|
+
* - llm_active_stat
|
|
6
|
+
* - llm_api_status
|
|
7
|
+
* - llm_error_stat
|
|
8
|
+
* - llm_feature_stat
|
|
9
|
+
* - llm_perf_stat (sketch fields omitted; Node has no datasketches)
|
|
10
|
+
* - llm_token_usage
|
|
11
|
+
* - meter (LLM_TRANSACTION)
|
|
12
|
+
*
|
|
13
|
+
* Each task exposes a process() method invoked by counter-manager.
|
|
14
|
+
* Packs are sent via async_sender.send_llm_relaypack to the LLM Go agent.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TagCountPack = require('../../pack/tagcount-pack');
|
|
18
|
+
const DataOutputX = require('../../io/data-outputx');
|
|
19
|
+
const AsyncSender = require('../../udp/async_sender');
|
|
20
|
+
const conf = require('../../conf/configure');
|
|
21
|
+
const Logger = require('../../logger');
|
|
22
|
+
const HashUtil = require('../../util/hashutil');
|
|
23
|
+
const collector = require('../../llm/llm-stat-collector');
|
|
24
|
+
const { getLlmPcode } = require('../../llm/llm-pcode');
|
|
25
|
+
|
|
26
|
+
const _PID = process.pid;
|
|
27
|
+
|
|
28
|
+
function _enabled() {
|
|
29
|
+
return conf.getProperty('llm_enabled', false);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _bucketTime(ms) {
|
|
33
|
+
return Math.floor(ms / 5000) * 5000;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _send(p) {
|
|
37
|
+
try {
|
|
38
|
+
p.pcode = getLlmPcode();
|
|
39
|
+
const bout = new DataOutputX();
|
|
40
|
+
bout.writePack(p, null);
|
|
41
|
+
AsyncSender.send_llm_relaypack(bout.toByteArray());
|
|
42
|
+
} catch (e) {
|
|
43
|
+
Logger.printError('WHATAP-LLM-STAT-001', 'send pack failed', e, false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _newPack(category, model, provider, opType, url) {
|
|
48
|
+
const p = new TagCountPack();
|
|
49
|
+
p.time = _bucketTime(Date.now());
|
|
50
|
+
p.category = category;
|
|
51
|
+
p.tags.putLong('pid', _PID);
|
|
52
|
+
p.tags.putString('model', model || 'unknown');
|
|
53
|
+
p.tags.putString('provider', provider || '');
|
|
54
|
+
p.tags.putString('operation_type', opType || 'unknown');
|
|
55
|
+
p.tags.putString('url', url || '');
|
|
56
|
+
return p;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------
|
|
60
|
+
// 1) llm_active_stat
|
|
61
|
+
// ---------------------------------------------------------
|
|
62
|
+
function LLMActiveStatTask() {}
|
|
63
|
+
LLMActiveStatTask.prototype.process = function () {
|
|
64
|
+
if (!_enabled()) return;
|
|
65
|
+
// 매 tick에 model cache TTL 만료 entry 제거 (10분/500개 cap)
|
|
66
|
+
try { collector.pruneModelCache(); } catch (e) {}
|
|
67
|
+
|
|
68
|
+
const snap = collector.snapshotAndResetActive();
|
|
69
|
+
if (snap.active.size === 0) return;
|
|
70
|
+
snap.active.forEach((count, k) => {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = collector.parseKey(k);
|
|
73
|
+
const info = snap.modelInfo.get(parsed.model);
|
|
74
|
+
const provider = info ? info.provider : '';
|
|
75
|
+
const url = info ? info.url : '';
|
|
76
|
+
const p = _newPack('llm_active_stat', parsed.model, provider, parsed.op_type, url);
|
|
77
|
+
p.fields.putLong('count', count);
|
|
78
|
+
_send(p);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
Logger.printError('WHATAP-LLM-STAT-010', 'active stat error', e, false);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------
|
|
86
|
+
// 2) llm_api_status
|
|
87
|
+
// ---------------------------------------------------------
|
|
88
|
+
function LLMApiStatusTask() {}
|
|
89
|
+
LLMApiStatusTask.prototype.process = function () {
|
|
90
|
+
if (!_enabled()) return;
|
|
91
|
+
const snap = collector.snapshotAndResetApiStatus();
|
|
92
|
+
if (snap.x4.size === 0 && snap.x5.size === 0) return;
|
|
93
|
+
|
|
94
|
+
// Set spread → Map iteration으로 GC 압박 감소 (intermediate array 제거)
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
const emit = (k) => {
|
|
97
|
+
if (seen.has(k)) return;
|
|
98
|
+
seen.add(k);
|
|
99
|
+
try {
|
|
100
|
+
const parsed = collector.parseKey(k);
|
|
101
|
+
const p = _newPack('llm_api_status', parsed.model, parsed.provider, parsed.op_type, parsed.url);
|
|
102
|
+
p.fields.putLong('4xx_total_count', snap.x4.get(k) || 0);
|
|
103
|
+
p.fields.putLong('5xx_total_count', snap.x5.get(k) || 0);
|
|
104
|
+
_send(p);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
Logger.printError('WHATAP-LLM-STAT-020', 'api status error', e, false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
snap.x4.forEach((_, k) => emit(k));
|
|
110
|
+
snap.x5.forEach((_, k) => emit(k));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------
|
|
114
|
+
// 3) llm_error_stat
|
|
115
|
+
// ---------------------------------------------------------
|
|
116
|
+
function LLMErrorStatTask() {}
|
|
117
|
+
LLMErrorStatTask.prototype.process = function () {
|
|
118
|
+
if (!_enabled()) return;
|
|
119
|
+
const snap = collector.snapshotAndResetErrors();
|
|
120
|
+
if (snap.api.size === 0 && snap.program.size === 0 && snap.lastApi.size === 0) return;
|
|
121
|
+
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const emit = (k) => {
|
|
124
|
+
if (seen.has(k)) return;
|
|
125
|
+
seen.add(k);
|
|
126
|
+
try {
|
|
127
|
+
const parsed = collector.parseKey(k);
|
|
128
|
+
const apiE = snap.api.get(k) || 0;
|
|
129
|
+
const progE = snap.program.get(k) || 0;
|
|
130
|
+
const p = _newPack('llm_error_stat', parsed.model, parsed.provider, parsed.op_type, parsed.url);
|
|
131
|
+
p.fields.putLong('error_count', apiE + progE);
|
|
132
|
+
p.fields.putLong('api_error_count', apiE);
|
|
133
|
+
p.fields.putLong('program_error_count', progE);
|
|
134
|
+
p.fields.putLong('last_api_error_count', snap.lastApi.get(k) || 0);
|
|
135
|
+
_send(p);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
Logger.printError('WHATAP-LLM-STAT-030', 'error stat error', e, false);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
snap.api.forEach((_, k) => emit(k));
|
|
141
|
+
snap.program.forEach((_, k) => emit(k));
|
|
142
|
+
snap.lastApi.forEach((_, k) => emit(k));
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------
|
|
146
|
+
// 4) llm_feature_stat (리스트형 → !rectype=2)
|
|
147
|
+
// ---------------------------------------------------------
|
|
148
|
+
function LLMFeatureStatTask() {}
|
|
149
|
+
LLMFeatureStatTask.prototype.process = function () {
|
|
150
|
+
if (!_enabled()) return;
|
|
151
|
+
const snap = collector.snapshotAndResetFeatures();
|
|
152
|
+
if (snap.calls.size === 0) return;
|
|
153
|
+
snap.calls.forEach((callCount, k) => {
|
|
154
|
+
try {
|
|
155
|
+
const parsed = collector.parseKey(k);
|
|
156
|
+
const p = _newPack('llm_feature_stat', parsed.model, parsed.provider, parsed.op_type, parsed.url);
|
|
157
|
+
p.tags.putLong('!rectype', 2);
|
|
158
|
+
p.fields.putLong('call_count', callCount);
|
|
159
|
+
|
|
160
|
+
const idList = p.fields.newList('@id');
|
|
161
|
+
const fList = p.fields.newList('features');
|
|
162
|
+
const fCntList = p.fields.newList('features_count');
|
|
163
|
+
const counts = snap.counts.get(k) || {};
|
|
164
|
+
for (const feat of collector.FEATURES) {
|
|
165
|
+
const c = counts[feat] || 0;
|
|
166
|
+
if (c > 0) {
|
|
167
|
+
idList.addLong(HashUtil.hashFromString(`${parsed.model}:${parsed.provider}:${parsed.op_type}:${feat}`));
|
|
168
|
+
fList.addString(feat);
|
|
169
|
+
fCntList.addLong(c);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
_send(p);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
Logger.printError('WHATAP-LLM-STAT-040', 'feature stat error', e, false);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------
|
|
180
|
+
// 5) llm_perf_stat (sketch 미지원 — sum/count만 전송)
|
|
181
|
+
// ---------------------------------------------------------
|
|
182
|
+
function LLMPerfStatTask() {}
|
|
183
|
+
LLMPerfStatTask.prototype.process = function () {
|
|
184
|
+
if (!_enabled()) return;
|
|
185
|
+
const snap = collector.snapshotAndResetPerf();
|
|
186
|
+
if (snap.callCount.size === 0) return;
|
|
187
|
+
snap.callCount.forEach((call, k) => {
|
|
188
|
+
try {
|
|
189
|
+
const parsed = collector.parseKey(k);
|
|
190
|
+
const p = _newPack('llm_perf_stat', parsed.model, parsed.provider, parsed.op_type, parsed.url);
|
|
191
|
+
p.fields.putLong('call_count', call);
|
|
192
|
+
p.fields.putFloat('latency_sum', snap.latencySum.get(k) || 0);
|
|
193
|
+
p.fields.putFloat('ttft_sum', snap.ttftSum.get(k) || 0);
|
|
194
|
+
p.fields.putLong('ttft_count', snap.ttftCount.get(k) || 0);
|
|
195
|
+
p.fields.putFloat('tpot_sum', snap.tpotSum.get(k) || 0);
|
|
196
|
+
p.fields.putLong('tpot_count', snap.tpotCount.get(k) || 0);
|
|
197
|
+
// sketch 필드는 datasketches 미지원으로 omit
|
|
198
|
+
_send(p);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
Logger.printError('WHATAP-LLM-STAT-050', 'perf stat error', e, false);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------
|
|
206
|
+
// 6) llm_token_usage (리스트형 → !rectype=2)
|
|
207
|
+
// ---------------------------------------------------------
|
|
208
|
+
const _TOKEN_TYPES = ['input_tokens', 'output_tokens', 'cached_tokens', 'reasoning_tokens',
|
|
209
|
+
'cache_creation_input_tokens', 'cache_read_input_tokens'];
|
|
210
|
+
const _TOKEN_COST_MAP = {
|
|
211
|
+
'input_tokens': 'input_cost',
|
|
212
|
+
'output_tokens': 'output_cost',
|
|
213
|
+
'cached_tokens': 'cached_cost',
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
function LLMTokenUsageTask() {}
|
|
217
|
+
LLMTokenUsageTask.prototype.process = function () {
|
|
218
|
+
if (!_enabled()) return;
|
|
219
|
+
const snap = collector.snapshotAndResetTokens();
|
|
220
|
+
if (snap.calls.size === 0) return;
|
|
221
|
+
snap.calls.forEach((call, k) => {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = collector.parseKey(k);
|
|
224
|
+
const counts = snap.counts.get(k) || {};
|
|
225
|
+
const costs = snap.costs.get(k) || {};
|
|
226
|
+
const totalTokens = (counts.input_tokens || 0) + (counts.output_tokens || 0);
|
|
227
|
+
const totalCost = (costs.input_cost || 0) + (costs.output_cost || 0);
|
|
228
|
+
|
|
229
|
+
const p = _newPack('llm_token_usage', parsed.model, parsed.provider, parsed.op_type, parsed.url);
|
|
230
|
+
p.tags.putLong('!rectype', 2);
|
|
231
|
+
p.fields.putLong('call_count', call);
|
|
232
|
+
p.fields.putLong('total_tokens', totalTokens);
|
|
233
|
+
p.fields.putFloat('total_cost', +totalCost.toFixed(6));
|
|
234
|
+
p.fields.putLong('error_count', snap.error.get(k) || 0);
|
|
235
|
+
p.fields.putLong('stream_count', snap.stream.get(k) || 0);
|
|
236
|
+
|
|
237
|
+
const idList = p.fields.newList('@id');
|
|
238
|
+
const tokList = p.fields.newList('tokens');
|
|
239
|
+
const cntList = p.fields.newList('tokens_count');
|
|
240
|
+
const costList = p.fields.newList('tokens_cost');
|
|
241
|
+
for (const tt of _TOKEN_TYPES) {
|
|
242
|
+
const c = counts[tt] || 0;
|
|
243
|
+
if (c > 0) {
|
|
244
|
+
idList.addLong(HashUtil.hashFromString(`${parsed.model}:${parsed.provider}:${parsed.op_type}:${tt}`));
|
|
245
|
+
tokList.addString(tt);
|
|
246
|
+
cntList.addLong(c);
|
|
247
|
+
const costKey = _TOKEN_COST_MAP[tt];
|
|
248
|
+
costList.addFloat(costKey ? +(costs[costKey] || 0).toFixed(6) : 0);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
_send(p);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
Logger.printError('WHATAP-LLM-STAT-060', 'token usage error', e, false);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// LLM 호출 카운트는 _llmMeterCount로 유지되어 ACTIVE_STATS 패킷 꼬리에
|
|
259
|
+
// "LLM:count=N" 형태로 append 송신됨 (udp_session.js:161).
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
LLMActiveStatTask,
|
|
263
|
+
LLMApiStatusTask,
|
|
264
|
+
LLMErrorStatTask,
|
|
265
|
+
LLMFeatureStatTask,
|
|
266
|
+
LLMPerfStatTask,
|
|
267
|
+
LLMTokenUsageTask,
|
|
268
|
+
};
|
package/lib/io/data-outputx.js
CHANGED
|
@@ -275,7 +275,7 @@ DataOutputX.prototype.writeInt40BE = function(value) {
|
|
|
275
275
|
return this.writeLong5(value);
|
|
276
276
|
};
|
|
277
277
|
|
|
278
|
-
// 새롭게 구현된 writeLong
|
|
278
|
+
// 새롭게 구현된 writeLong
|
|
279
279
|
DataOutputX.prototype.writeLong = function(value) {
|
|
280
280
|
if (isNumOk(value) == false) {
|
|
281
281
|
value = 0;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BigInt 변환 캐싱 유틸.
|
|
5
|
+
* JS Number의 53bit 초과 정수 정밀도 손실 방지용 BigInt 변환은 비용이 있는 작업이므로
|
|
6
|
+
* 한 라이프사이클 내 동일 값에 대해 1회만 수행.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function bigStr(v) {
|
|
10
|
+
if (v == null) return '';
|
|
11
|
+
try {
|
|
12
|
+
return BigInt(v).toString();
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return String(v);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ctx.id를 BigInt-precise string으로 반환. ctx에 _txidStr로 캐싱.
|
|
20
|
+
*/
|
|
21
|
+
function ctxTxidStr(ctx) {
|
|
22
|
+
if (!ctx) return '';
|
|
23
|
+
if (ctx._txidStr) return ctx._txidStr;
|
|
24
|
+
ctx._txidStr = bigStr(ctx.id);
|
|
25
|
+
return ctx._txidStr;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 임의 step_id를 BigInt-precise string으로 반환. 라이프사이클 짧으면 캐싱 안 함.
|
|
30
|
+
*/
|
|
31
|
+
function stepIdStr(stepIdRaw) {
|
|
32
|
+
return bigStr(stepIdRaw);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { bigStr, ctxTxidStr, stepIdStr };
|