whatap 0.5.26 → 0.5.27
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 +77 -0
- package/lib/conf/config-default.js +1 -1
- package/lib/counter/task/gc-action.js +31 -21
- package/lib/counter/task/metering-info.js +38 -3
- package/lib/counter/task/systemperf.js +4 -1
- package/lib/observers/http-observer.js +13 -8
- package/lib/observers/process-observer.js +1 -1
- package/lib/pack/hitmap-pack1.js +2 -1
- package/lib/util/cgroup-cpu.js +78 -0
- package/package.json +2 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
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)",
|
|
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 $? ---\")",
|
|
12
|
+
"Bash(go version *)",
|
|
13
|
+
"WebFetch(domain:github.com)",
|
|
14
|
+
"Read(//Users/seunghunlee/workspace/**)",
|
|
15
|
+
"Read(//Users/seunghunlee/workspace/nodejs_docker_sample/**)",
|
|
16
|
+
"Bash(npm i *)",
|
|
17
|
+
"Bash(chmod +x /Users/seunghunlee/workspace/nodejs_docker_sample/repro.sh)",
|
|
18
|
+
"Bash(docker exec 273976be9e71 *)",
|
|
19
|
+
"Bash(docker rm *)",
|
|
20
|
+
"Bash(docker build *)",
|
|
21
|
+
"Bash(docker run *)",
|
|
22
|
+
"Bash(docker exec zombie-test *)",
|
|
23
|
+
"Bash(curl -s http://localhost:3000/)",
|
|
24
|
+
"Bash(curl -s http://localhost:3000/health)",
|
|
25
|
+
"Bash(curl -s http://localhost:3000/slow)",
|
|
26
|
+
"Bash(curl -s -o /dev/null -w \"status: %{http_code}\\\\n\" http://localhost:3000/error)",
|
|
27
|
+
"Bash(curl -s -o /dev/null -w \"%{http_code} \" http://localhost:3000/)",
|
|
28
|
+
"Bash(/usr/local/bin/docker exec *)",
|
|
29
|
+
"Bash(/usr/local/bin/docker build *)",
|
|
30
|
+
"Bash(/usr/local/bin/docker rm *)",
|
|
31
|
+
"Bash(/usr/local/bin/docker run *)",
|
|
32
|
+
"Bash(/bin/sleep 30)",
|
|
33
|
+
"Bash(echo \"[$\\(date +%T\\)] zombies=$\\(/usr/local/bin/docker exec zombie-test sh -c \"ps -eo stat | awk '\\\\$1 ~ /^Z/' | wc -l\"\\)\")",
|
|
34
|
+
"Bash(/bin/sleep 5)",
|
|
35
|
+
"Bash(/usr/local/bin/docker ps *)",
|
|
36
|
+
"Bash(/bin/sleep 6)",
|
|
37
|
+
"Bash(/usr/local/bin/docker logs *)",
|
|
38
|
+
"Bash(/usr/bin/curl -s http://localhost:3000/)",
|
|
39
|
+
"Bash(/usr/bin/curl -s http://localhost:3000/health)",
|
|
40
|
+
"Bash(/bin/sleep 7)",
|
|
41
|
+
"Bash(cat agent/index.js)",
|
|
42
|
+
"Bash(cat agent/writer.js)",
|
|
43
|
+
"Bash(node -e \"const d=require\\('./config/defaults.js'\\); console.log\\('flushInterval=', d.flushInterval\\)\")",
|
|
44
|
+
"Bash(cd *)",
|
|
45
|
+
"Bash(ls /Users/seunghunlee/workspace/nodejs_docker_sample/node_modules/whatap/lib)",
|
|
46
|
+
"Bash(ls /Users/seunghunlee/workspace/nodejs_docker_sample/node_modules/whatap/index.js)",
|
|
47
|
+
"Bash(node -e ' *)",
|
|
48
|
+
"Bash(export WHATAP_HOME=/tmp)",
|
|
49
|
+
"Bash(node --expose-gc -e ' *)",
|
|
50
|
+
"Bash(node -p \"process.platform+'/'+process.arch+' node '+process.version\")",
|
|
51
|
+
"Read(//Users/seunghunlee/agent_workspace/apm-go-agent/**)",
|
|
52
|
+
"Bash(node -e \"require\\('./lib/counter/task/metering-info'\\); console.log\\('module load OK'\\);\")",
|
|
53
|
+
"Bash(node --check lib/counter/task/metering-info.js)",
|
|
54
|
+
"Bash(node --check lib/util/cgroup-cpu.js)",
|
|
55
|
+
"Bash(node -e \"var m=require\\('./index.js'\\); console.log\\('exports keys:', Object.keys\\(m\\)\\);\")",
|
|
56
|
+
"Bash(docker version *)",
|
|
57
|
+
"Bash(rm -rf whatap-local)",
|
|
58
|
+
"Bash(rsync -a --exclude=.git --exclude=node_modules --exclude=logs --exclude=*.log /Users/seunghunlee/agent_workspace/nodejs_agent/ ./whatap-local/)",
|
|
59
|
+
"Bash(node *)",
|
|
60
|
+
"Bash(npm view *)",
|
|
61
|
+
"Bash(rsync -a --delete --exclude=.git --exclude=node_modules --exclude=logs --exclude=*.log /Users/seunghunlee/agent_workspace/nodejs_agent/ ./whatap-local/)",
|
|
62
|
+
"Bash(docker exec *)",
|
|
63
|
+
"Bash(curl -s --max-time 5 http://localhost:3001/health)",
|
|
64
|
+
"Bash(curl -s --max-time 5 http://localhost:3002/health)",
|
|
65
|
+
"Bash(sed -n '66,71p' lib/counter/counter-manager.js)",
|
|
66
|
+
"Bash(sed -n '166,168p' lib/conf/configure.js)",
|
|
67
|
+
"Bash(sed -n '40,70p' lib/kube/kube-client.js)",
|
|
68
|
+
"Bash(docker info *)",
|
|
69
|
+
"Bash(« stray *)",
|
|
70
|
+
"Bash(awk '{print $1}')",
|
|
71
|
+
"Bash(xargs -r docker rm -f)",
|
|
72
|
+
"Read(//Users/seunghunlee/agent_workspace/**)",
|
|
73
|
+
"Bash(net_udp_packet_queue_size=500 node -e ' *)",
|
|
74
|
+
"Bash(max_send_queue_size=500 node -e ' *)"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -284,7 +284,7 @@ var ConfigDefault = {
|
|
|
284
284
|
"txtext_txname_enabled": bool("txtext_txname_enabled", true),
|
|
285
285
|
"ignore_env_variable_set": str("ignore_env_variable_set", ""),
|
|
286
286
|
"profile_httpc_start_step_enabled": bool("profile_httpc_start_step_enabled", false),
|
|
287
|
-
"perfx_nodejs_gc_enabled": bool("perfx_nodejs_gc_enabled",
|
|
287
|
+
"perfx_nodejs_gc_enabled": bool("perfx_nodejs_gc_enabled", false)
|
|
288
288
|
|
|
289
289
|
};
|
|
290
290
|
|
|
@@ -54,31 +54,41 @@ GCAction.prototype.nodejs_memory_gc = function (p) {
|
|
|
54
54
|
|
|
55
55
|
try {
|
|
56
56
|
// PerformanceObserver를 사용해서 현재까지의 GC 데이터 수집
|
|
57
|
+
// 콜백은 비동기로 실행되어 바깥 try/catch가 못 잡으므로, 예외가 고객 앱의
|
|
58
|
+
// uncaughtException으로 전파되지 않도록 콜백 전체를 보호한다.
|
|
57
59
|
const observer = new PerformanceObserver((list) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
gcStats.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
try {
|
|
61
|
+
list.getEntries().forEach((entry) => {
|
|
62
|
+
if (entry.entryType === 'gc') {
|
|
63
|
+
const gcName = self.getGCTypeName(entry.kind);
|
|
64
|
+
|
|
65
|
+
if (!gcStats.has(gcName)) {
|
|
66
|
+
gcStats.set(gcName, {
|
|
67
|
+
name: gcName,
|
|
68
|
+
collectionCount: 0,
|
|
69
|
+
collectionTime: 0
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const stats = gcStats.get(gcName);
|
|
74
|
+
stats.collectionCount++;
|
|
75
|
+
stats.collectionTime += entry.duration;
|
|
68
76
|
}
|
|
77
|
+
});
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
stats.collectionTime += entry.duration;
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// 데이터 수집 완료 후 즉시 처리
|
|
77
|
-
observer.disconnect();
|
|
79
|
+
// 데이터 수집 완료 후 즉시 처리
|
|
80
|
+
observer.disconnect();
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
// 수집된 데이터가 있으면 전송
|
|
83
|
+
if (gcStats.size > 0) {
|
|
84
|
+
self.sendGCData(p, gcStats);
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
try {
|
|
88
|
+
observer.disconnect();
|
|
89
|
+
} catch (e2) {
|
|
90
|
+
}
|
|
91
|
+
Logger.printError("WHATAP-GC-005", "GC observer callback error", e, false);
|
|
82
92
|
}
|
|
83
93
|
});
|
|
84
94
|
|
|
@@ -13,6 +13,8 @@ var CounterTask = require('./counter-task'),
|
|
|
13
13
|
TagCountPack = require('../../pack/tagcount-pack'),
|
|
14
14
|
OType = require('../../pack/otype'),
|
|
15
15
|
APEnum = require('../../pack/apenum'),
|
|
16
|
+
SecurityMaster = require('../../net/security-master'),
|
|
17
|
+
CgroupCpu = require('../../util/cgroup-cpu'),
|
|
16
18
|
Util = require('../../util/index');
|
|
17
19
|
|
|
18
20
|
function MeteringInfo() {
|
|
@@ -28,7 +30,7 @@ MeteringInfo.prototype = new CounterTask();
|
|
|
28
30
|
MeteringInfo.prototype.constructor = MeteringInfo;
|
|
29
31
|
|
|
30
32
|
MeteringInfo.prototype.process = function (p) {
|
|
31
|
-
if (!
|
|
33
|
+
if (!this.isMeteringEnabled()) {
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -47,8 +49,40 @@ MeteringInfo.prototype.process = function (p) {
|
|
|
47
49
|
this.doProcess(p);
|
|
48
50
|
};
|
|
49
51
|
|
|
52
|
+
// metering_container_enabled=true이면 (host product_uuid 없이도) 메터링을 전송한다.
|
|
53
|
+
MeteringInfo.prototype.isMeteringEnabled = function () {
|
|
54
|
+
return conf.getProperty('metering_tagcount_enabled', false) === true
|
|
55
|
+
|| this.isContainerMeteringEnabled();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
MeteringInfo.prototype.isContainerMeteringEnabled = function () {
|
|
59
|
+
return CgroupCpu.isContainerMeteringEnabled();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// metering_container_enabled=true이면 root 권한이 필요한 host product_uuid 대신
|
|
63
|
+
// OID hex를 컨테이너별 고유 식별자로 사용한다.
|
|
64
|
+
// (Go 에이전트 SecurityMaster: fmt.Sprintf("%08x", uint32(OID)) 와 동일)
|
|
65
|
+
MeteringInfo.prototype.resolveHostUUID = function () {
|
|
66
|
+
if (this.isContainerMeteringEnabled()) {
|
|
67
|
+
return this.getContainerUUID();
|
|
68
|
+
}
|
|
69
|
+
return this.getHostUUID(this.hostUUIDFileName);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
MeteringInfo.prototype.getContainerUUID = function () {
|
|
73
|
+
const oid = SecurityMaster.OID;
|
|
74
|
+
if (!oid) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
var hex = (oid >>> 0).toString(16);
|
|
78
|
+
while (hex.length < 8) {
|
|
79
|
+
hex = '0' + hex;
|
|
80
|
+
}
|
|
81
|
+
return hex;
|
|
82
|
+
};
|
|
83
|
+
|
|
50
84
|
MeteringInfo.prototype.doProcess = function (p) {
|
|
51
|
-
const host_uuid = this.
|
|
85
|
+
const host_uuid = this.resolveHostUUID();
|
|
52
86
|
if(!host_uuid)
|
|
53
87
|
return;
|
|
54
88
|
|
|
@@ -67,7 +101,8 @@ MeteringInfo.prototype.doProcess = function (p) {
|
|
|
67
101
|
pk.putTagString('csp', csp)
|
|
68
102
|
}
|
|
69
103
|
|
|
70
|
-
|
|
104
|
+
// Go TagTaskMetering.go: p.Put("mcore", sys.GetCPUNum())
|
|
105
|
+
pk.putTagFloat('mcore', CgroupCpu.getCpuNum())
|
|
71
106
|
|
|
72
107
|
DataPackSender.sendTagCounterPack(pk);
|
|
73
108
|
};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
var CounterTask = require('./counter-task'),
|
|
8
8
|
ResourceProfile = require('./../../util/resourceprofile'),
|
|
9
9
|
KubeUtil = require('./../../util/kube-util'),
|
|
10
|
+
CgroupCpu = require('./../../util/cgroup-cpu'),
|
|
10
11
|
SecurityMaster = require('../../net/security-master');
|
|
11
12
|
|
|
12
13
|
function SystemPerf(){
|
|
@@ -30,7 +31,9 @@ SystemPerf.prototype.process = function(p) {
|
|
|
30
31
|
if(p.version <= 1) p.mem = ResourceProfile.getMemory().usage;
|
|
31
32
|
else p.mem = ResourceProfile.getMemoryV2().usage;
|
|
32
33
|
|
|
33
|
-
p.
|
|
34
|
+
// Go TaskSystemPerf.go: p.CpuCores = sys.GetCPUNum()
|
|
35
|
+
// (getCpuNum 내부에서 metering_container_enabled 판정: true+cgroup한도 → ceil, 그 외 → 호스트 CPU 개수)
|
|
36
|
+
p.cpu_cores = CgroupCpu.getCpuNum();
|
|
34
37
|
p.host_ip = SecurityMaster.IP;
|
|
35
38
|
p.pid=process.pid;
|
|
36
39
|
|
|
@@ -182,6 +182,19 @@ HttpObserver.prototype.__createTransactionObserver = function(callback, isHttps,
|
|
|
182
182
|
return callback(req, res);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
if(ctx.isStaticContents !== true && conf._trace_ignore_url_set[ctx.service_hash]){
|
|
186
|
+
ctx.isStaticContents = true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if(ctx.isStaticContents !== true && conf._is_trace_ignore_url_prefix === true && ctx.service_name.startsWith(conf.trace_ignore_url_prefix) === true){
|
|
190
|
+
ctx.isStaticContents = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if(ctx.isStaticContents){
|
|
194
|
+
TraceContextManager.end(ctx._id);
|
|
195
|
+
return callback(req, res);
|
|
196
|
+
}
|
|
197
|
+
|
|
185
198
|
PluginLoaderManager.do('httpservicestart', ctx, req, res);
|
|
186
199
|
if(trace_origin_url === true){
|
|
187
200
|
var originUrlHash = HashUtil.hashFromString('Origin url');
|
|
@@ -604,14 +617,6 @@ HttpObserver.prototype.__endTransaction = function(error, ctx, req, res) {
|
|
|
604
617
|
|
|
605
618
|
// if(ctx == null || TraceContextManager.isExist(ctx._id) === false) { return; }
|
|
606
619
|
|
|
607
|
-
if(ctx.isStaticContents !== true && conf._trace_ignore_url_set[ctx.service_hash]){
|
|
608
|
-
ctx.isStaticContents = true;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if(conf._is_trace_ignore_url_prefix === true && ctx.service_name.startsWith(conf.trace_ignore_url_prefix) === true){
|
|
612
|
-
ctx.isStaticContents = true;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
620
|
if(ctx.isStaticContents){
|
|
616
621
|
TraceContextManager.end(ctx._id);
|
|
617
622
|
ctx = null;
|
|
@@ -27,7 +27,7 @@ ProcessObserver.prototype.inject = function (mod, moduleName) {
|
|
|
27
27
|
this._hookNextTick(mod);
|
|
28
28
|
this._hookStdOutWrite();
|
|
29
29
|
this._hookStdErrWrite();
|
|
30
|
-
this._hookProcessExit(mod);
|
|
30
|
+
// this._hookProcessExit(mod);
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
ProcessObserver.prototype._hookNextTick = function (mod) {
|
package/lib/pack/hitmap-pack1.js
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2025 the WHATAP project authors. All rights reserved.
|
|
3
|
+
* Use of this source code is governed by a license that
|
|
4
|
+
* can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 컨테이너(cgroup) CPU limit 조회.
|
|
9
|
+
* Go 에이전트 util/sys/CpuUtil.go 의 getCgroupCpuLimitFloat() / GetCPUNum() 와 동일한 로직.
|
|
10
|
+
*/
|
|
11
|
+
var fs = require('fs'),
|
|
12
|
+
os = require('os');
|
|
13
|
+
|
|
14
|
+
// cgroup CPU limit 을 코어수(float)로 반환한다. 한도 미설정/감지 실패 시 0.
|
|
15
|
+
// cgroup v2: /sys/fs/cgroup/cpu.max -> "$MAX $PERIOD" (예: "200000 100000" = 2 cores), "max 100000" = 무제한
|
|
16
|
+
// cgroup v1: cpu.cfs_quota_us / cpu.cfs_period_us (quota -1 = 무제한)
|
|
17
|
+
function getCgroupCpuLimitFloat() {
|
|
18
|
+
if (process.platform !== 'linux') {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// cgroup v2
|
|
23
|
+
try {
|
|
24
|
+
var data = fs.readFileSync('/sys/fs/cgroup/cpu.max', 'utf8').trim();
|
|
25
|
+
var fields = data.split(/\s+/);
|
|
26
|
+
if (fields.length === 2 && fields[0] !== 'max') {
|
|
27
|
+
var quotaV2 = parseFloat(fields[0]);
|
|
28
|
+
var periodV2 = parseFloat(fields[1]);
|
|
29
|
+
if (!isNaN(quotaV2) && !isNaN(periodV2) && periodV2 > 0) {
|
|
30
|
+
return quotaV2 / periodV2;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// v2 파일은 읽혔으나 한도 미설정("max")이거나 형식 불일치 -> 한도 없음
|
|
34
|
+
return 0;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// v2 파일이 없으면 v1 으로 폴백
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// cgroup v1
|
|
40
|
+
try {
|
|
41
|
+
var quotaData = fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8').trim();
|
|
42
|
+
var periodData = fs.readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8').trim();
|
|
43
|
+
var quota = parseFloat(quotaData);
|
|
44
|
+
var period = parseFloat(periodData);
|
|
45
|
+
if (isNaN(quota) || isNaN(period) || period <= 0 || quota <= 0) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
return quota / period;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// metering_container_enabled 옵션 여부 (config 또는 env). 컨테이너 메터링 공통 판정.
|
|
55
|
+
function isContainerMeteringEnabled() {
|
|
56
|
+
var conf = require('../conf/configure');
|
|
57
|
+
return conf.getProperty('metering_container_enabled', false) === true
|
|
58
|
+
|| process.env.WHATAP_METERING_CONTAINER_ENABLED === 'true';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 미터링/CPU 코어수. Go sys.GetCPUNum() 와 1:1 동일:
|
|
62
|
+
// metering_container_enabled=true 이고 cgroup 한도가 있으면 ceil 한 정수,
|
|
63
|
+
// 그 외(플래그 off 또는 한도 없음)는 호스트 CPU 개수(= runtime.NumCPU()).
|
|
64
|
+
function getCpuNum() {
|
|
65
|
+
if (isContainerMeteringEnabled()) {
|
|
66
|
+
var cgroupCores = getCgroupCpuLimitFloat();
|
|
67
|
+
if (cgroupCores > 0) {
|
|
68
|
+
return Math.ceil(cgroupCores);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return os.cpus().length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
getCgroupCpuLimitFloat: getCgroupCpuLimitFloat,
|
|
76
|
+
getCpuNum: getCpuNum,
|
|
77
|
+
isContainerMeteringEnabled: isContainerMeteringEnabled
|
|
78
|
+
};
|
package/package.json
CHANGED