wobs-js 0.1.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 +54 -0
- package/SUMMARY.md +70 -0
- package/examples/demo.js +60 -0
- package/package.json +25 -0
- package/src/fileTrace.js +329 -0
- package/src/observer.js +103 -0
- package/src/trackpoint.js +224 -0
- package/src/userInfo.js +458 -0
- package/trace_test_panxiangpeng.trace +2 -0
- package/trackpoint_test_panxiangpeng.trackpoint +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Wuying GuestOS Observer JavaScript
|
|
2
|
+
|
|
3
|
+
## 功能
|
|
4
|
+
* 记录埋点和Trace数据
|
|
5
|
+
* 埋点的数据格式是JSON对象,包含埋点名称(eventName)、时间(time)、属性(properties)和一些其它的配置信息,比如实例名称、版本等信息。
|
|
6
|
+
* Trace数据格式是JSON对象,包含TraceID、SpanID、ParentSpanID、Name、Kind、StartTime、EndTime、Duration、Attributes、Events、Links、StatusCode、StatusMessage等信息。以SLS Trace数据格式输出到本地文件。
|
|
7
|
+
|
|
8
|
+
## 使用方法
|
|
9
|
+
1. 创建一个Node.js项目
|
|
10
|
+
2. 使用示例可以参考examples/demo.js
|
|
11
|
+
|
|
12
|
+
```javascript
|
|
13
|
+
const { init, shutdown, newSpanAsCurrent, newTrackPoint } = require('wobs-js');
|
|
14
|
+
|
|
15
|
+
if (require.main === module) {
|
|
16
|
+
// 初始化observer,指定trackpoint和trace文件存放目录,这里设置为当前路径,默认可不填,和C++、Golang版本保持一致
|
|
17
|
+
init("test", './', './');
|
|
18
|
+
|
|
19
|
+
// 创建一个span,并设置属性和事件
|
|
20
|
+
const span = newSpanAsCurrent("test_trace");
|
|
21
|
+
// 设置属性
|
|
22
|
+
span.setAttribute("key", "value");
|
|
23
|
+
// 添加事件
|
|
24
|
+
span.addEvent("event1", {"event_attr": "event_value"});
|
|
25
|
+
// 如果失败了,设置trace的状态和错误信息
|
|
26
|
+
span.setStatus(getStatus(false), "error message");
|
|
27
|
+
// 记录一个埋点
|
|
28
|
+
newTrackPoint("test_trace");
|
|
29
|
+
span.end();
|
|
30
|
+
|
|
31
|
+
// 程序结束后关闭observer,这一步是可选的
|
|
32
|
+
shutdown();
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 如何使用远端的trace_id创建span
|
|
37
|
+
创建span时,可以指定trace_id和span_id,这样trace_id和span_id就会作为span的父span,从而实现链路追踪。
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const span = newSpanAsCurrent("test_trace", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxx");
|
|
41
|
+
span.setAttribute("key", "value");
|
|
42
|
+
span.end();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 依赖
|
|
46
|
+
* Node.js >= 12.0.0
|
|
47
|
+
|
|
48
|
+
## Windows平台支持
|
|
49
|
+
在Windows平台上,本SDK支持从注册表读取用户和系统信息,包括:
|
|
50
|
+
* 阿里云EDS Agent相关信息
|
|
51
|
+
* Windows版本信息
|
|
52
|
+
* 用户桌面和实例信息
|
|
53
|
+
|
|
54
|
+
注册表读取通过Node.js的child_process模块调用Windows的reg命令实现,无需额外的依赖包。
|
package/SUMMARY.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# JavaScript版本实现总结
|
|
2
|
+
|
|
3
|
+
## 实现概述
|
|
4
|
+
本项目实现了Python版本Wuying Observer SDK的JavaScript版本,功能完全一致,包括:
|
|
5
|
+
|
|
6
|
+
1. **UserInfo模块** - 获取用户和系统信息
|
|
7
|
+
2. **FileSpanExporter** - 将Trace数据输出到本地文件
|
|
8
|
+
3. **TrackPointManager** - 记录埋点数据
|
|
9
|
+
4. **Observer接口** - 提供统一的API
|
|
10
|
+
|
|
11
|
+
## 功能对比
|
|
12
|
+
|
|
13
|
+
| 功能 | Python版本 | JavaScript版本 | 状态 |
|
|
14
|
+
|------|------------|----------------|------|
|
|
15
|
+
| 用户信息获取 | ✅ | ✅ | 完成 |
|
|
16
|
+
| Trace数据导出 | ✅ | ✅ | 完成 |
|
|
17
|
+
| 埋点数据记录 | ✅ | ✅ | 完成 |
|
|
18
|
+
| 文件轮转 | ✅ | ✅ | 完成 |
|
|
19
|
+
| OpenTelemetry集成 | ✅ | ✅ | 完成 |
|
|
20
|
+
|
|
21
|
+
## 使用方法
|
|
22
|
+
|
|
23
|
+
### 初始化
|
|
24
|
+
```javascript
|
|
25
|
+
const { init } = require('./src/observer');
|
|
26
|
+
init('service_name', './trackpoint_dir', './trace_dir');
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 创建Trace Span
|
|
30
|
+
```javascript
|
|
31
|
+
const { newSpanAsCurrent } = require('./src/observer');
|
|
32
|
+
const span = newSpanAsCurrent('span_name');
|
|
33
|
+
span.setAttribute('key', 'value');
|
|
34
|
+
span.addEvent('event_name', { attr: 'value' });
|
|
35
|
+
span.end();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 记录埋点
|
|
39
|
+
```javascript
|
|
40
|
+
const { newTrackPoint } = require('./src/observer');
|
|
41
|
+
newTrackPoint('event_name', { properties: 'value' });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 文件结构
|
|
45
|
+
```
|
|
46
|
+
js/
|
|
47
|
+
├── src/
|
|
48
|
+
│ ├── userInfo.js # 用户信息模块
|
|
49
|
+
│ ├── fileTrace.js # Trace导出模块
|
|
50
|
+
│ ├── trackpoint.js # 埋点记录模块
|
|
51
|
+
│ └── observer.js # Observer接口
|
|
52
|
+
├── examples/
|
|
53
|
+
│ └── demo.js # 使用示例
|
|
54
|
+
├── package.json # 项目配置
|
|
55
|
+
└── README.md # 使用说明
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 测试结果
|
|
59
|
+
已成功测试以下功能:
|
|
60
|
+
1. Trace数据正确导出到`.trace`文件
|
|
61
|
+
2. 埋点数据正确记录到`.trackpoint`文件
|
|
62
|
+
3. 文件轮转功能正常工作
|
|
63
|
+
4. 用户信息正确获取并附加到数据中
|
|
64
|
+
|
|
65
|
+
## 平台支持
|
|
66
|
+
- Windows
|
|
67
|
+
- Linux
|
|
68
|
+
- macOS
|
|
69
|
+
|
|
70
|
+
与Python版本保持一致的跨平台支持。
|
package/examples/demo.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript demo for Wuying Observer SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { init, shutdown, newSpanAsCurrent, newTrackPoint, getStatus } = require('../src/observer');
|
|
6
|
+
|
|
7
|
+
function testTrace() {
|
|
8
|
+
const traceIdHex = '0af7651916cd43dd8448eb211c80319c'; // 32 hex
|
|
9
|
+
const spanIdHex = 'b9c7c989f97918e1'; // 16 hex
|
|
10
|
+
|
|
11
|
+
// 创建一个span,并设置属性和事件
|
|
12
|
+
const span = newSpanAsCurrent('test_span', traceIdHex);
|
|
13
|
+
span.setAttribute('key', 'value');
|
|
14
|
+
span.setAttribute('key2', 'value2');
|
|
15
|
+
span.setStatus(getStatus(false));
|
|
16
|
+
span.addEvent('event1', { event_attr: 'event_value' });
|
|
17
|
+
|
|
18
|
+
// 记录一个埋点
|
|
19
|
+
newTrackPoint('test_trace');
|
|
20
|
+
|
|
21
|
+
console.log(`In span: ${span.name}`);
|
|
22
|
+
span.end();
|
|
23
|
+
console.log('Out of span');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function testException() {
|
|
27
|
+
const span = newSpanAsCurrent('test_exception');
|
|
28
|
+
span.setAttribute('key', 'value');
|
|
29
|
+
span.addEvent('event1', { event_attr: 'event_value' });
|
|
30
|
+
newTrackPoint('test_exception');
|
|
31
|
+
|
|
32
|
+
// 模拟一个异常,异常发生后,Trace中会自动记录异常信息和堆栈
|
|
33
|
+
span.end();
|
|
34
|
+
throw new Error('raise exception test');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function testRotation() {
|
|
38
|
+
const a = 10;
|
|
39
|
+
console.log('start test_rotation', new Date().getTime());
|
|
40
|
+
for (let i = 0; i < a; i++) {
|
|
41
|
+
const span = newSpanAsCurrent(`test_rotation_${i}`);
|
|
42
|
+
span.setAttribute('key', 'value');
|
|
43
|
+
span.addEvent('event1', { event_attr: 'event_value' });
|
|
44
|
+
newTrackPoint(`test_rotation_${i}`);
|
|
45
|
+
span.end();
|
|
46
|
+
}
|
|
47
|
+
console.log('end test_rotation', new Date().getTime());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 主程序
|
|
51
|
+
if (require.main === module) {
|
|
52
|
+
// 初始化observer,指定trackpoint和trace文件存放目录,这里设置为当前路径,默认可不填,和C++、Golang版本保持一致
|
|
53
|
+
init('builder');
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
newTrackPoint('test', { args: process.argv });
|
|
57
|
+
|
|
58
|
+
// 程序结束后关闭observer,这一步是可选的
|
|
59
|
+
shutdown();
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wobs-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Wuying Observer SDK for JavaScript",
|
|
5
|
+
"main": "src/observer.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node examples/demo.js",
|
|
8
|
+
"demo": "node examples/demo.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"opentelemetry",
|
|
12
|
+
"trace",
|
|
13
|
+
"trackpoint",
|
|
14
|
+
"monitoring"
|
|
15
|
+
],
|
|
16
|
+
"author": "Wuying",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"directories": {
|
|
19
|
+
"example": "examples",
|
|
20
|
+
"src": "src"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=12.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/fileTrace.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript implementation of FileSpanExporter for OpenTelemetry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { getDefaultObserverConfig, getUserInfo, appendUserInfo } = require('./userInfo');
|
|
8
|
+
|
|
9
|
+
class FileSpanExporter {
|
|
10
|
+
/** Exporter that writes spans to a local file with rotation support. */
|
|
11
|
+
|
|
12
|
+
constructor(serviceName = 'unknown_service', filePath = null, maxBytes = null, backupCount = null) {
|
|
13
|
+
const config = getDefaultObserverConfig();
|
|
14
|
+
this.filePath = filePath || config.TRACE;
|
|
15
|
+
this.filename = `trace_${serviceName}_${this._getUsername()}.trace`;
|
|
16
|
+
this.maxBytes = maxBytes || config.DEFAULT_MAX_FILE_SIZE;
|
|
17
|
+
this.backupCount = backupCount || config.DEFAULT_MAX_FILES;
|
|
18
|
+
|
|
19
|
+
// Ensure the directory exists
|
|
20
|
+
if (!fs.existsSync(this.filePath)) {
|
|
21
|
+
fs.mkdirSync(this.filePath, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Initialize resource information
|
|
25
|
+
this.resource = {};
|
|
26
|
+
|
|
27
|
+
// Initialize file stream
|
|
28
|
+
this.fileStream = null;
|
|
29
|
+
this.currentFileSize = 0;
|
|
30
|
+
this._initializeFileStream();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_getUsername() {
|
|
34
|
+
/** Get the current username. */
|
|
35
|
+
try {
|
|
36
|
+
return require('os').userInfo().username;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return process.env.USER || process.env.USERNAME || '';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_initializeFileStream() {
|
|
43
|
+
/** Initialize the file stream for writing. */
|
|
44
|
+
const fullPath = path.join(this.filePath, this.filename);
|
|
45
|
+
|
|
46
|
+
// Check if file exists and get its size
|
|
47
|
+
if (fs.existsSync(fullPath)) {
|
|
48
|
+
const stats = fs.statSync(fullPath);
|
|
49
|
+
this.currentFileSize = stats.size;
|
|
50
|
+
} else {
|
|
51
|
+
this.currentFileSize = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Open file in append mode
|
|
55
|
+
this.fileStream = fs.createWriteStream(fullPath, { flags: 'a' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_rotateFile() {
|
|
59
|
+
/** Rotate the log file if it exceeds maxBytes. */
|
|
60
|
+
if (this.currentFileSize >= this.maxBytes) {
|
|
61
|
+
this.fileStream.close();
|
|
62
|
+
|
|
63
|
+
const fullPath = path.join(this.filePath, this.filename);
|
|
64
|
+
|
|
65
|
+
// Rotate existing files
|
|
66
|
+
for (let i = this.backupCount - 1; i > 0; i--) {
|
|
67
|
+
const oldFile = `${fullPath}.${i}`;
|
|
68
|
+
const newFile = `${fullPath}.${i + 1}`;
|
|
69
|
+
|
|
70
|
+
if (fs.existsSync(oldFile)) {
|
|
71
|
+
if (fs.existsSync(newFile)) {
|
|
72
|
+
fs.unlinkSync(newFile);
|
|
73
|
+
}
|
|
74
|
+
fs.renameSync(oldFile, newFile);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Rename current file to .1
|
|
79
|
+
const firstBackup = `${fullPath}.1`;
|
|
80
|
+
if (fs.existsSync(firstBackup)) {
|
|
81
|
+
fs.unlinkSync(firstBackup);
|
|
82
|
+
}
|
|
83
|
+
fs.renameSync(fullPath, firstBackup);
|
|
84
|
+
|
|
85
|
+
// Create new file
|
|
86
|
+
this.currentFileSize = 0;
|
|
87
|
+
this.fileStream = fs.createWriteStream(fullPath, { flags: 'w' });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export(spans) {
|
|
92
|
+
/** Export spans to a local file with rotation support. */
|
|
93
|
+
try {
|
|
94
|
+
// Process each span
|
|
95
|
+
for (const span of spans) {
|
|
96
|
+
// Build data according to trace_data_format
|
|
97
|
+
const spanDict = {
|
|
98
|
+
host: this.resource.host || '',
|
|
99
|
+
service: this.resource.service || 'unknown',
|
|
100
|
+
resource: this.resource,
|
|
101
|
+
name: span.name || '',
|
|
102
|
+
kind: span.kind || '',
|
|
103
|
+
traceID: span.spanContext ? span.spanContext.traceId : '',
|
|
104
|
+
spanID: span.spanContext ? span.spanContext.spanId : '',
|
|
105
|
+
parentSpanID: span.parentSpanId || '',
|
|
106
|
+
links: span.links ? span.links.map(link => ({
|
|
107
|
+
TraceID: link.context ? link.context.traceId : '',
|
|
108
|
+
SpanId: link.context ? link.context.spanId : '',
|
|
109
|
+
TraceState: '',
|
|
110
|
+
Attributes: link.attributes || {}
|
|
111
|
+
})) : [],
|
|
112
|
+
logs: span.events ? span.events.map(event => ({
|
|
113
|
+
Name: event.name || '',
|
|
114
|
+
Time: event.timestamp ? Number(event.timestamp) : 0,
|
|
115
|
+
attribute: event.attributes || {}
|
|
116
|
+
})) : [],
|
|
117
|
+
traceState: '',
|
|
118
|
+
start: span.startTimeUnixNano ? Number(span.startTimeUnixNano) / 1000 : 0,
|
|
119
|
+
end: span.endTimeUnixNano ? Number(span.endTimeUnixNano) / 1000 : 0,
|
|
120
|
+
duration: span.duration ? Number(span.duration) / 1000 : 0,
|
|
121
|
+
attribute: span.attributes || {},
|
|
122
|
+
statusCode: span.status ? span.status.code : 'UNSET',
|
|
123
|
+
statusMessage: span.status ? span.status.message : ''
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Write to file
|
|
127
|
+
this._writeToFile(JSON.stringify(spanDict));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { code: 0 }; // Success
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('Export failed:', error);
|
|
133
|
+
return { code: 2 }; // FailedNotRetryable
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_writeToFile(jsonStr) {
|
|
138
|
+
/** Write JSON string to file with rotation. */
|
|
139
|
+
this._rotateFile();
|
|
140
|
+
|
|
141
|
+
const data = jsonStr + '\n';
|
|
142
|
+
this.fileStream.write(data);
|
|
143
|
+
this.currentFileSize += Buffer.byteLength(data);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
shutdown() {
|
|
147
|
+
/** Shutdown the exporter. */
|
|
148
|
+
if (this.fileStream) {
|
|
149
|
+
this.fileStream.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setResource(resource) {
|
|
154
|
+
/** Set the resource for the exporter. */
|
|
155
|
+
this.resource = resource;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
class TraceManager {
|
|
160
|
+
/** Trace 管理接口类 */
|
|
161
|
+
|
|
162
|
+
constructor() {
|
|
163
|
+
this.tracerProvider = null;
|
|
164
|
+
this.exporter = null;
|
|
165
|
+
this.processor = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setupTracerProvider(exporter, serviceName = 'unknown_service') {
|
|
169
|
+
/**
|
|
170
|
+
* 设置TracerProvider和相关组件
|
|
171
|
+
*
|
|
172
|
+
* @param {FileSpanExporter} exporter - Trace导出器
|
|
173
|
+
* @param {string} serviceName - 服务名称
|
|
174
|
+
* @returns {Object} TracerProvider实例
|
|
175
|
+
*/
|
|
176
|
+
// In a real implementation, we would integrate with OpenTelemetry JS SDK
|
|
177
|
+
// For this implementation, we'll just store the references
|
|
178
|
+
|
|
179
|
+
this.exporter = exporter;
|
|
180
|
+
|
|
181
|
+
// Set resource with user info
|
|
182
|
+
const resDict = {
|
|
183
|
+
'service.name': serviceName
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const userInfo = getUserInfo();
|
|
187
|
+
const userInfoFields = {};
|
|
188
|
+
appendUserInfo(userInfoFields);
|
|
189
|
+
|
|
190
|
+
for (const [key, value] of Object.entries(userInfoFields)) {
|
|
191
|
+
resDict[`env.${key}`] = value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
exporter.setResource(resDict);
|
|
195
|
+
|
|
196
|
+
return this.tracerProvider;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getTracer(name = 'default') {
|
|
200
|
+
/**
|
|
201
|
+
* 获取tracer
|
|
202
|
+
*
|
|
203
|
+
* @param {string} name - tracer名称
|
|
204
|
+
* @returns {Object} Tracer实例 (simulated)
|
|
205
|
+
*/
|
|
206
|
+
// In a real implementation, we would return an actual OpenTelemetry tracer
|
|
207
|
+
// For this implementation, we'll return a simulated tracer
|
|
208
|
+
return {
|
|
209
|
+
startSpan: (spanName, options = {}) => {
|
|
210
|
+
const spanContext = {
|
|
211
|
+
traceId: options.root ? this._generateTraceId() : (options.parentContext?.traceId || this._generateTraceId()),
|
|
212
|
+
spanId: this._generateSpanId(),
|
|
213
|
+
traceFlags: 1
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Create the span object
|
|
217
|
+
const span = {
|
|
218
|
+
name: spanName,
|
|
219
|
+
spanContext: spanContext,
|
|
220
|
+
parentSpanId: options.parentContext?.spanId || '',
|
|
221
|
+
attributes: {},
|
|
222
|
+
events: [],
|
|
223
|
+
links: [],
|
|
224
|
+
startTimeUnixNano: process.hrtime.bigint(),
|
|
225
|
+
status: { code: 'UNSET' },
|
|
226
|
+
ended: false,
|
|
227
|
+
|
|
228
|
+
setAttribute: function(key, value) {
|
|
229
|
+
this.attributes[key] = value;
|
|
230
|
+
return this; // Allow chaining
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
setAttributes: function(attrs) {
|
|
234
|
+
Object.assign(this.attributes, attrs);
|
|
235
|
+
return this; // Allow chaining
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
addEvent: function(name, attributes = {}) {
|
|
239
|
+
this.events.push({
|
|
240
|
+
name: name,
|
|
241
|
+
attributes: attributes,
|
|
242
|
+
timestamp: process.hrtime.bigint()
|
|
243
|
+
});
|
|
244
|
+
return this; // Allow chaining
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
setStatus: function(status) {
|
|
248
|
+
this.status = status;
|
|
249
|
+
return this; // Allow chaining
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
end: function() {
|
|
253
|
+
if (this.ended) return;
|
|
254
|
+
this.ended = true;
|
|
255
|
+
this.endTimeUnixNano = process.hrtime.bigint();
|
|
256
|
+
this.duration = this.endTimeUnixNano - this.startTimeUnixNano;
|
|
257
|
+
|
|
258
|
+
// Export the span
|
|
259
|
+
if (this.exporter) {
|
|
260
|
+
this.exporter.export([this]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Bind exporter to span for use in end()
|
|
266
|
+
span.exporter = this.exporter;
|
|
267
|
+
|
|
268
|
+
return span;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
startActiveSpan: function(spanName, options, context, fn) {
|
|
272
|
+
// Simplified implementation
|
|
273
|
+
if (typeof options === 'function') {
|
|
274
|
+
fn = options;
|
|
275
|
+
options = {};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const span = this.startSpan(spanName, options);
|
|
279
|
+
try {
|
|
280
|
+
return fn(span);
|
|
281
|
+
} finally {
|
|
282
|
+
span.end();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_generateTraceId() {
|
|
289
|
+
/** Generate a random trace ID. */
|
|
290
|
+
return Math.random().toString(16).substr(2, 32).padStart(32, '0');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_generateSpanId() {
|
|
294
|
+
/** Generate a random span ID. */
|
|
295
|
+
return Math.random().toString(16).substr(2, 16).padStart(16, '0');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
forceFlush() {
|
|
299
|
+
/** 强制刷新处理器以确保导出 */
|
|
300
|
+
// In a real implementation, we would flush the processor
|
|
301
|
+
// For this implementation, we'll just ensure the file is flushed
|
|
302
|
+
if (this.exporter) {
|
|
303
|
+
// The file stream should flush automatically, but we can close and reopen
|
|
304
|
+
this.exporter.shutdown();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
shutdown() {
|
|
309
|
+
/** 关闭所有组件 */
|
|
310
|
+
if (this.exporter) {
|
|
311
|
+
this.exporter.shutdown();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function initTraceManager(serviceName = 'unknown_service', filePath = null, maxBytes = null, backupCount = null) {
|
|
317
|
+
/** 初始化TraceManager */
|
|
318
|
+
const exporter = new FileSpanExporter(serviceName, filePath, maxBytes, backupCount);
|
|
319
|
+
const traceManager = new TraceManager();
|
|
320
|
+
traceManager.setupTracerProvider(exporter, serviceName);
|
|
321
|
+
traceManager.exporter = exporter; // Make exporter accessible to tracer
|
|
322
|
+
return traceManager;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
FileSpanExporter,
|
|
327
|
+
TraceManager,
|
|
328
|
+
initTraceManager
|
|
329
|
+
};
|
package/src/observer.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript implementation of Observer interface
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { initUserInfo } = require('./userInfo');
|
|
6
|
+
const { initTrackpointManager, trackEvent } = require('./trackpoint');
|
|
7
|
+
const { initTraceManager } = require('./fileTrace');
|
|
8
|
+
|
|
9
|
+
let _traceManager = null;
|
|
10
|
+
|
|
11
|
+
function setConfig(config) {
|
|
12
|
+
// Placeholder for configuration setting
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function init(moduleName, trackPointDir = null, traceDir = null) {
|
|
16
|
+
initUserInfo();
|
|
17
|
+
_traceManager = initTraceManager(moduleName, traceDir);
|
|
18
|
+
initTrackpointManager(moduleName, trackPointDir);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function shutdown() {
|
|
22
|
+
if (_traceManager) {
|
|
23
|
+
_traceManager.forceFlush();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tracer() {
|
|
28
|
+
if (_traceManager) {
|
|
29
|
+
return _traceManager.getTracer();
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function newSpan(name, traceId = null, spanId = null) {
|
|
35
|
+
/**
|
|
36
|
+
* 创建一个新的span
|
|
37
|
+
* @param {string} name - span名称
|
|
38
|
+
* @param {string} traceId - trace_id,如果传入则使用该trace_id,否则生成新的,格式为32位16进制字符串
|
|
39
|
+
* @param {string} spanId - span_id,如果传入了trace_id,则使用该span_id作为parentspanid,格式为16位16进制字符串
|
|
40
|
+
* @returns {Object} Span对象
|
|
41
|
+
*/
|
|
42
|
+
if (_traceManager) {
|
|
43
|
+
const tracer = _traceManager.getTracer();
|
|
44
|
+
if (traceId) {
|
|
45
|
+
// In a real OpenTelemetry implementation, we would create a context with the provided traceId and spanId
|
|
46
|
+
// For this simplified implementation, we'll pass them as options
|
|
47
|
+
return tracer.startSpan(name, {
|
|
48
|
+
root: true,
|
|
49
|
+
parentContext: {
|
|
50
|
+
traceId: traceId,
|
|
51
|
+
spanId: spanId || 'ffffffffffffffff'
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return tracer.startSpan(name);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function newSpanAsCurrent(name, traceId = null, spanId = null) {
|
|
61
|
+
/**
|
|
62
|
+
* 创建一个新的span,并设置该span为当前span
|
|
63
|
+
* @param {string} name - span名称
|
|
64
|
+
* @param {string} traceId - trace_id,如果传入则使用该trace_id,否则生成新的,格式为32位16进制字符串
|
|
65
|
+
* @param {string} spanId - span_id,如果传入了trace_id,则使用该span_id作为parentspanid,格式为16位16进制字符串
|
|
66
|
+
* @returns {Object} Span对象
|
|
67
|
+
*/
|
|
68
|
+
if (_traceManager) {
|
|
69
|
+
const tracer = _traceManager.getTracer();
|
|
70
|
+
if (traceId) {
|
|
71
|
+
// In a real OpenTelemetry implementation, we would create a context with the provided traceId and spanId
|
|
72
|
+
// For this simplified implementation, we'll pass them as options
|
|
73
|
+
return tracer.startSpan(name, {
|
|
74
|
+
root: true,
|
|
75
|
+
parentContext: {
|
|
76
|
+
traceId: traceId,
|
|
77
|
+
spanId: spanId || 'ffffffffffffffff'
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return tracer.startSpan(name);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function newTrackPoint(eventName, properties = null) {
|
|
87
|
+
return trackEvent(eventName, properties);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getStatus(success = false) {
|
|
91
|
+
return success ? { code: 'OK' } : { code: 'ERROR' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
setConfig,
|
|
96
|
+
init,
|
|
97
|
+
shutdown,
|
|
98
|
+
tracer,
|
|
99
|
+
newSpan,
|
|
100
|
+
newSpanAsCurrent,
|
|
101
|
+
newTrackPoint,
|
|
102
|
+
getStatus
|
|
103
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript implementation of TrackPoint functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { getDefaultObserverConfig, appendUserInfo } = require('./userInfo');
|
|
8
|
+
|
|
9
|
+
class TrackPointManager {
|
|
10
|
+
/** Manages track point logging with file rotation */
|
|
11
|
+
|
|
12
|
+
constructor(moduleName = 'unknown', trackpointDir = null, maxFileSize = null, maxFiles = null) {
|
|
13
|
+
/**
|
|
14
|
+
* Initialize TrackPointManager.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} moduleName - Name of the module
|
|
17
|
+
* @param {string} trackpointDir - Directory to store trackpoint files
|
|
18
|
+
* @param {number} maxFileSize - Maximum size of each trackpoint file in bytes
|
|
19
|
+
* @param {number} maxFiles - Maximum number of trackpoint files to maintain
|
|
20
|
+
*/
|
|
21
|
+
const config = getDefaultObserverConfig();
|
|
22
|
+
this.trackpointDir = trackpointDir || config.TRACKPOINT;
|
|
23
|
+
this.maxFileSize = maxFileSize || config.DEFAULT_MAX_FILE_SIZE;
|
|
24
|
+
this.maxFiles = maxFiles || config.DEFAULT_MAX_FILES;
|
|
25
|
+
this.moduleName = moduleName;
|
|
26
|
+
this.fileName = `trackpoint_${moduleName}_${this._getUsername()}.trackpoint`;
|
|
27
|
+
|
|
28
|
+
// Ensure the directory exists
|
|
29
|
+
if (!fs.existsSync(this.trackpointDir)) {
|
|
30
|
+
fs.mkdirSync(this.trackpointDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// File stream for writing
|
|
34
|
+
this.fileStream = null;
|
|
35
|
+
this.currentFileSize = 0;
|
|
36
|
+
this._initializeFileStream();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_getUsername() {
|
|
40
|
+
/** Get the current username. */
|
|
41
|
+
try {
|
|
42
|
+
return require('os').userInfo().username;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return process.env.USER || process.env.USERNAME || '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_initializeFileStream() {
|
|
49
|
+
/** Initialize the file stream for writing. */
|
|
50
|
+
const fullPath = path.join(this.trackpointDir, this.fileName);
|
|
51
|
+
|
|
52
|
+
// Check if file exists and get its size
|
|
53
|
+
if (fs.existsSync(fullPath)) {
|
|
54
|
+
const stats = fs.statSync(fullPath);
|
|
55
|
+
this.currentFileSize = stats.size;
|
|
56
|
+
} else {
|
|
57
|
+
this.currentFileSize = 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Open file in append mode
|
|
61
|
+
this.fileStream = fs.createWriteStream(fullPath, { flags: 'a' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_rotateFile() {
|
|
65
|
+
/** Rotate the log file if it exceeds maxFileSize. */
|
|
66
|
+
if (this.currentFileSize >= this.maxFileSize) {
|
|
67
|
+
this.fileStream.close();
|
|
68
|
+
|
|
69
|
+
const fullPath = path.join(this.trackpointDir, this.fileName);
|
|
70
|
+
|
|
71
|
+
// Rotate existing files
|
|
72
|
+
for (let i = this.maxFiles - 1; i > 0; i--) {
|
|
73
|
+
const oldFile = `${fullPath}.${i}`;
|
|
74
|
+
const newFile = `${fullPath}.${i + 1}`;
|
|
75
|
+
|
|
76
|
+
if (fs.existsSync(oldFile)) {
|
|
77
|
+
if (fs.existsSync(newFile)) {
|
|
78
|
+
fs.unlinkSync(newFile);
|
|
79
|
+
}
|
|
80
|
+
fs.renameSync(oldFile, newFile);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Rename current file to .1
|
|
85
|
+
const firstBackup = `${fullPath}.1`;
|
|
86
|
+
if (fs.existsSync(firstBackup)) {
|
|
87
|
+
fs.unlinkSync(firstBackup);
|
|
88
|
+
}
|
|
89
|
+
fs.renameSync(fullPath, firstBackup);
|
|
90
|
+
|
|
91
|
+
// Create new file
|
|
92
|
+
this.currentFileSize = 0;
|
|
93
|
+
this.fileStream = fs.createWriteStream(fullPath, { flags: 'w' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logEvent(eventName, properties = null, traceId = '') {
|
|
98
|
+
/**
|
|
99
|
+
* Log an event with its properties to trackpoint files.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} eventName - Name of the event being tracked
|
|
102
|
+
* @param {Object} properties - Dictionary of properties associated with the event
|
|
103
|
+
* @param {string} traceId - Trace ID to associate with the event
|
|
104
|
+
*/
|
|
105
|
+
// Get current timestamp with milliseconds in ISO format
|
|
106
|
+
const now = new Date();
|
|
107
|
+
const timestampMs = now.getMilliseconds();
|
|
108
|
+
const offset = -now.getTimezoneOffset() / 60;
|
|
109
|
+
const offsetStr = offset >= 0 ? `+${offset.toString().padStart(2, '0')}00` : `${offset.toString().padStart(3, '0')}00`;
|
|
110
|
+
const formattedTime = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}T${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}.${timestampMs.toString().padStart(3, '0')}${offsetStr}`;
|
|
111
|
+
|
|
112
|
+
// Prepare the event data
|
|
113
|
+
const eventData = {
|
|
114
|
+
time: formattedTime,
|
|
115
|
+
eventName: eventName,
|
|
116
|
+
module: this.moduleName
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Add trace ID if provided
|
|
120
|
+
if (traceId) {
|
|
121
|
+
eventData.traceId = traceId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add user info to the top level of event data
|
|
125
|
+
const userProperties = {};
|
|
126
|
+
appendUserInfo(userProperties);
|
|
127
|
+
|
|
128
|
+
// Filter out empty user properties and add them to the main event data
|
|
129
|
+
for (const [key, value] of Object.entries(userProperties)) {
|
|
130
|
+
if (value) {
|
|
131
|
+
eventData[key] = value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Add custom properties in a nested 'properties' field
|
|
136
|
+
if (properties) {
|
|
137
|
+
eventData.properties = properties;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Convert to JSON string
|
|
141
|
+
const jsonLine = JSON.stringify(eventData) + '\n';
|
|
142
|
+
this._writeToFile(jsonLine);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_writeToFile(jsonLine) {
|
|
146
|
+
/** Write JSON line to file with rotation. */
|
|
147
|
+
this._rotateFile();
|
|
148
|
+
|
|
149
|
+
this.fileStream.write(jsonLine);
|
|
150
|
+
this.currentFileSize += Buffer.byteLength(jsonLine);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
flush() {
|
|
154
|
+
/** Flush the trackpoint logger handlers. */
|
|
155
|
+
if (this.fileStream) {
|
|
156
|
+
this.fileStream.flush && this.fileStream.flush();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
shutdown() {
|
|
161
|
+
/** Shutdown the trackpoint manager. */
|
|
162
|
+
if (this.fileStream) {
|
|
163
|
+
this.fileStream.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Global instance for trackpoint management
|
|
169
|
+
let _trackpointManager = null;
|
|
170
|
+
|
|
171
|
+
function initTrackpointManager(moduleName, trackpointDir = null, maxFileSize = null, maxFiles = null) {
|
|
172
|
+
/**
|
|
173
|
+
* Initialize the trackpoint manager.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} moduleName - Name of the module
|
|
176
|
+
* @param {string} trackpointDir - Directory to store trackpoint files
|
|
177
|
+
* @param {number} maxFileSize - Maximum size of each trackpoint file in bytes
|
|
178
|
+
* @param {number} maxFiles - Maximum number of trackpoint files to maintain
|
|
179
|
+
* @returns {TrackPointManager} TrackPointManager instance
|
|
180
|
+
*/
|
|
181
|
+
if (_trackpointManager === null) {
|
|
182
|
+
_trackpointManager = new TrackPointManager(moduleName, trackpointDir, maxFileSize, maxFiles);
|
|
183
|
+
}
|
|
184
|
+
return _trackpointManager;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function trackEvent(eventName, properties = null, traceId = '') {
|
|
188
|
+
/**
|
|
189
|
+
* Log an event with its properties to trackpoint files.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} eventName - Name of the event being tracked
|
|
192
|
+
* @param {Object} properties - Dictionary of properties associated with the event
|
|
193
|
+
* @param {string} traceId - Trace ID to associate with the event
|
|
194
|
+
*/
|
|
195
|
+
// Initialize manager if not already done
|
|
196
|
+
if (_trackpointManager === null) {
|
|
197
|
+
console.warn('Warning: TrackPointManager not initialized. Call initTrackpointManager() first.');
|
|
198
|
+
return 'Warning: TrackPointManager not initialized. Call initTrackpointManager() first.';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_trackpointManager.logEvent(eventName, properties, traceId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function trackFlush() {
|
|
205
|
+
/** Flush the trackpoint logger handlers. */
|
|
206
|
+
if (_trackpointManager) {
|
|
207
|
+
_trackpointManager.flush();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function trackShutdown() {
|
|
212
|
+
/** Shutdown the trackpoint manager. */
|
|
213
|
+
if (_trackpointManager) {
|
|
214
|
+
_trackpointManager.shutdown();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
TrackPointManager,
|
|
220
|
+
initTrackpointManager,
|
|
221
|
+
trackEvent,
|
|
222
|
+
trackFlush,
|
|
223
|
+
trackShutdown
|
|
224
|
+
};
|
package/src/userInfo.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript implementation of userInfo functionality, compatible with different operating systems.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class ObserverConfig {
|
|
10
|
+
/** Configuration for Observer paths based on platform. */
|
|
11
|
+
constructor() {
|
|
12
|
+
// Set platform-specific paths
|
|
13
|
+
if (os.platform() === 'win32') {
|
|
14
|
+
this.basePath = 'C:\\ProgramData\\wuying\\observer\\';
|
|
15
|
+
} else if (os.platform() === 'android') {
|
|
16
|
+
this.basePath = '/data/vendor/log/wuying/observer/';
|
|
17
|
+
} else { // Linux or other Unix-like systems
|
|
18
|
+
this.basePath = '/var/log/wuying/observer/';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.LOG = path.join(this.basePath, 'log/');
|
|
22
|
+
this.TRACKPOINT = path.join(this.basePath, 'trackpoint/');
|
|
23
|
+
this.TRACE = path.join(this.basePath, 'traces/');
|
|
24
|
+
this.DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
25
|
+
this.DEFAULT_MAX_FILES = 5;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getDefaultObserverConfig() {
|
|
30
|
+
/** Get Observer configuration based on current platform */
|
|
31
|
+
return new ObserverConfig();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class UserInfo {
|
|
35
|
+
/** Data class to hold user information. */
|
|
36
|
+
constructor() {
|
|
37
|
+
// ECS info
|
|
38
|
+
this.instanceID = '';
|
|
39
|
+
this.regionID = '';
|
|
40
|
+
|
|
41
|
+
// System info
|
|
42
|
+
this.desktopID = '';
|
|
43
|
+
this.desktopGroupID = '';
|
|
44
|
+
this.appInstanceGroupID = '';
|
|
45
|
+
this.fotaVersion = '';
|
|
46
|
+
this.imageVersion = '';
|
|
47
|
+
this.osEdition = ''; // Microsoft Windows Server 2019 Datacenter
|
|
48
|
+
this.osVersion = ''; // 10.0.17763
|
|
49
|
+
this.osBuild = ''; // 17763.2237
|
|
50
|
+
this.osType = '';
|
|
51
|
+
this.dsMode = '';
|
|
52
|
+
this.localHostName = '';
|
|
53
|
+
|
|
54
|
+
// User info
|
|
55
|
+
this.userName = '';
|
|
56
|
+
this.AliUID = '';
|
|
57
|
+
this.officeSiteID = '';
|
|
58
|
+
this.ownerAccountId = '';
|
|
59
|
+
this.appInstanceID = '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getNonEmptyValues() {
|
|
63
|
+
/**
|
|
64
|
+
* Returns a dictionary of all attributes that have non-empty values.
|
|
65
|
+
*
|
|
66
|
+
* @returns {Object} Dictionary containing attribute names and their non-empty values
|
|
67
|
+
*/
|
|
68
|
+
const result = {};
|
|
69
|
+
for (const [key, value] of Object.entries(this)) {
|
|
70
|
+
if (value !== '') {
|
|
71
|
+
result[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getOsType() {
|
|
79
|
+
/** Get the operating system type. */
|
|
80
|
+
const platform = os.platform();
|
|
81
|
+
if (platform === 'win32') {
|
|
82
|
+
return 'Windows';
|
|
83
|
+
} else if (platform === 'linux') {
|
|
84
|
+
return 'Linux';
|
|
85
|
+
} else if (platform === 'darwin') {
|
|
86
|
+
return 'macOS';
|
|
87
|
+
} else {
|
|
88
|
+
return 'Unknown';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getUsername() {
|
|
93
|
+
/** Get the current username. */
|
|
94
|
+
try {
|
|
95
|
+
return os.userInfo().username;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return process.env.USER || process.env.USERNAME || '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getOsVersion() {
|
|
102
|
+
/** Get OS version information. */
|
|
103
|
+
const platform = os.platform();
|
|
104
|
+
const release = os.release();
|
|
105
|
+
|
|
106
|
+
if (platform === 'win32') {
|
|
107
|
+
return `Windows ${release}`;
|
|
108
|
+
} else if (platform === 'linux') {
|
|
109
|
+
try {
|
|
110
|
+
// Try to get Linux version from /etc/os-release
|
|
111
|
+
const osRelease = fs.readFileSync('/etc/os-release', 'utf8');
|
|
112
|
+
const lines = osRelease.split('\n');
|
|
113
|
+
let name = '';
|
|
114
|
+
let version = '';
|
|
115
|
+
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
if (line.startsWith('NAME=')) {
|
|
118
|
+
name = line.split('=')[1].replace(/"/g, '').trim();
|
|
119
|
+
} else if (line.startsWith('VERSION_ID=')) {
|
|
120
|
+
version = line.split('=')[1].replace(/"/g, '').trim();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (name && version) {
|
|
125
|
+
return `${name} ${version}`;
|
|
126
|
+
} else {
|
|
127
|
+
return `Linux ${release}`;
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return `Linux ${release}`;
|
|
131
|
+
}
|
|
132
|
+
} else if (platform === 'darwin') {
|
|
133
|
+
return `macOS ${release}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return 'Unknown';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getSystemInfo() {
|
|
140
|
+
/** Get system information (osVersion, osType). */
|
|
141
|
+
return [getOsVersion(), getOsType()];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getUserInfoFromRegistry(userInfo) {
|
|
145
|
+
/** Get user info from Windows registry. */
|
|
146
|
+
if (require('os').platform() !== 'win32') {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const { execSync } = require('child_process');
|
|
152
|
+
|
|
153
|
+
// Read imageInfos
|
|
154
|
+
try {
|
|
155
|
+
const imageInfosOutput = execSync(
|
|
156
|
+
'reg query "HKLM\\SYSTEM\\CurrentControlSet\\Services\\AliyunEDSAgent\\imageInfos" /s',
|
|
157
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const lines = imageInfosOutput.split('\n');
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
if (line.includes('name')) {
|
|
163
|
+
const match = line.match(/name\s+REG_SZ\s+(.+)/);
|
|
164
|
+
if (match) {
|
|
165
|
+
userInfo.imageVersion = match[1].trim();
|
|
166
|
+
}
|
|
167
|
+
} else if (line.includes('fota_version')) {
|
|
168
|
+
const match = line.match(/fota_version\s+REG_SZ\s+(.+)/);
|
|
169
|
+
if (match) {
|
|
170
|
+
userInfo.fotaVersion = match[1].trim();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// Ignore registry read errors
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Read desktopInfos
|
|
179
|
+
try {
|
|
180
|
+
const desktopInfosOutput = execSync(
|
|
181
|
+
'reg query "HKLM\\SYSTEM\\CurrentControlSet\\Services\\AliyunEDSAgent\\desktopInfos" /s',
|
|
182
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const lines = desktopInfosOutput.split('\n');
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
if (line.includes('desktopId')) {
|
|
188
|
+
const match = line.match(/desktopId\s+REG_SZ\s+(.+)/);
|
|
189
|
+
if (match) {
|
|
190
|
+
userInfo.desktopID = match[1].trim();
|
|
191
|
+
}
|
|
192
|
+
} else if (line.includes('aliUid')) {
|
|
193
|
+
const match = line.match(/aliUid\s+REG_SZ\s+(.+)/);
|
|
194
|
+
if (match) {
|
|
195
|
+
userInfo.AliUID = match[1].trim();
|
|
196
|
+
}
|
|
197
|
+
} else if (line.includes('officeSiteId')) {
|
|
198
|
+
const match = line.match(/officeSiteId\s+REG_SZ\s+(.+)/);
|
|
199
|
+
if (match) {
|
|
200
|
+
userInfo.officeSiteID = match[1].trim();
|
|
201
|
+
}
|
|
202
|
+
} else if (line.includes('desktopGroupId')) {
|
|
203
|
+
const match = line.match(/desktopGroupId\s+REG_SZ\s+(.+)/);
|
|
204
|
+
if (match) {
|
|
205
|
+
userInfo.desktopGroupID = match[1].trim();
|
|
206
|
+
}
|
|
207
|
+
} else if (line.includes('appInstanceGroupId')) {
|
|
208
|
+
const match = line.match(/appInstanceGroupId\s+REG_SZ\s+(.+)/);
|
|
209
|
+
if (match) {
|
|
210
|
+
userInfo.appInstanceGroupID = match[1].trim();
|
|
211
|
+
}
|
|
212
|
+
} else if (line.includes('regionId')) {
|
|
213
|
+
const match = line.match(/regionId\s+REG_SZ\s+(.+)/);
|
|
214
|
+
if (match) {
|
|
215
|
+
userInfo.regionID = match[1].trim();
|
|
216
|
+
}
|
|
217
|
+
} else if (line.includes('instanceId')) {
|
|
218
|
+
const match = line.match(/instanceId\s+REG_SZ\s+(.+)/);
|
|
219
|
+
if (match) {
|
|
220
|
+
userInfo.instanceID = match[1].trim();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// Ignore registry read errors
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Read Windows version info
|
|
229
|
+
try {
|
|
230
|
+
const versionOutput = execSync(
|
|
231
|
+
'reg query "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion" /v ProductName',
|
|
232
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const match = versionOutput.match(/ProductName\s+REG_SZ\s+(.+)/);
|
|
236
|
+
if (match) {
|
|
237
|
+
userInfo.osEdition = match[1].trim();
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// Ignore registry read errors
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Get username
|
|
244
|
+
try {
|
|
245
|
+
userInfo.userName = getUsername();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
// Ignore
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.warn(`Failed to read from Windows registry: ${error.message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getUserInfoFromIni(userInfo) {
|
|
256
|
+
/** Get user info from INI-style config file (Linux/macOS). */
|
|
257
|
+
const os = require('os');
|
|
258
|
+
const fs = require('fs');
|
|
259
|
+
|
|
260
|
+
if (os.platform() === 'win32') {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Linux/Unix systems
|
|
265
|
+
const runtimeIniPath = '/etc/cloudstream/runtime.ini';
|
|
266
|
+
const imageInfoPath = '/etc/wuying/image_info.json';
|
|
267
|
+
const metaDataPath = '/var/lib/cloud/seed/nocloud/meta-data';
|
|
268
|
+
|
|
269
|
+
// Read from runtime.ini
|
|
270
|
+
if (fs.existsSync(runtimeIniPath)) {
|
|
271
|
+
try {
|
|
272
|
+
const data = fs.readFileSync(runtimeIniPath, 'utf8');
|
|
273
|
+
const lines = data.split('\n');
|
|
274
|
+
|
|
275
|
+
for (const line of lines) {
|
|
276
|
+
if (line.trim() && !line.startsWith('#')) {
|
|
277
|
+
const parts = line.trim().split('=', 2);
|
|
278
|
+
if (parts.length === 2) {
|
|
279
|
+
const [key, value] = parts;
|
|
280
|
+
if (key === 'DesktopId') {
|
|
281
|
+
userInfo.desktopID = value;
|
|
282
|
+
} else if (key === 'AliUid') {
|
|
283
|
+
userInfo.AliUID = value;
|
|
284
|
+
} else if (key === 'OfficeSiteId') {
|
|
285
|
+
userInfo.officeSiteID = value;
|
|
286
|
+
} else if (key === 'regionId') {
|
|
287
|
+
userInfo.regionID = value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.warn(`Failed to read ${runtimeIniPath}: ${error.message}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Read from image_info.json
|
|
298
|
+
if (fs.existsSync(imageInfoPath)) {
|
|
299
|
+
try {
|
|
300
|
+
const data = fs.readFileSync(imageInfoPath, 'utf8');
|
|
301
|
+
const jsonData = JSON.parse(data);
|
|
302
|
+
if (jsonData.fotaVersion) {
|
|
303
|
+
userInfo.fotaVersion = jsonData.fotaVersion;
|
|
304
|
+
}
|
|
305
|
+
if (jsonData.image_name) {
|
|
306
|
+
userInfo.imageVersion = jsonData.image_name;
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.warn(`Failed to read ${imageInfoPath}: ${error.message}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Read from meta-data
|
|
314
|
+
if (fs.existsSync(metaDataPath)) {
|
|
315
|
+
try {
|
|
316
|
+
const data = fs.readFileSync(metaDataPath, 'utf8');
|
|
317
|
+
const lines = data.split('\n');
|
|
318
|
+
const metaData = {};
|
|
319
|
+
|
|
320
|
+
for (const line of lines) {
|
|
321
|
+
const trimmedLine = line.trim();
|
|
322
|
+
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
|
323
|
+
// Split only on the first colon to handle values that contain colons
|
|
324
|
+
const colonIndex = trimmedLine.indexOf(':');
|
|
325
|
+
if (colonIndex > 0) {
|
|
326
|
+
const key = trimmedLine.substring(0, colonIndex).trim();
|
|
327
|
+
const value = trimmedLine.substring(colonIndex + 1).trim();
|
|
328
|
+
metaData[key] = value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (metaData['instance-id']) {
|
|
334
|
+
userInfo.instanceID = metaData['instance-id'];
|
|
335
|
+
}
|
|
336
|
+
if (metaData['dsmode']) {
|
|
337
|
+
userInfo.dsMode = metaData['dsmode'];
|
|
338
|
+
}
|
|
339
|
+
if (metaData['local-hostname']) {
|
|
340
|
+
userInfo.localHostName = metaData['local-hostname'];
|
|
341
|
+
}
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.warn(`Failed to read ${metaDataPath}: ${error.message}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function getUserInfoFromEnv(userInfo) {
|
|
349
|
+
try {
|
|
350
|
+
// 获取 ECS_INSTANCE_ID
|
|
351
|
+
const ecsInstanceId = process.env.ECS_INSTANCE_ID;
|
|
352
|
+
if (ecsInstanceId !== undefined) {
|
|
353
|
+
userInfo.instanceID = ecsInstanceId;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 获取 ACP_INSTANCE_ID
|
|
357
|
+
const appInstanceId = process.env.ACP_INSTANCE_ID;
|
|
358
|
+
if (appInstanceId !== undefined) {
|
|
359
|
+
userInfo.appInstanceID = appInstanceId;
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.warn(`Get instanceid failed, error: ${error.message}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getUserInfo() {
|
|
367
|
+
/** Get user information. */
|
|
368
|
+
const userInfo = new UserInfo();
|
|
369
|
+
|
|
370
|
+
// Get OS type and version
|
|
371
|
+
[userInfo.osVersion, userInfo.osType] = getSystemInfo();
|
|
372
|
+
|
|
373
|
+
// Get username
|
|
374
|
+
userInfo.userName = getUsername();
|
|
375
|
+
|
|
376
|
+
// Platform-specific information gathering
|
|
377
|
+
if (os.platform() === 'win32') {
|
|
378
|
+
// Windows-specific info gathering from registry
|
|
379
|
+
getUserInfoFromRegistry(userInfo);
|
|
380
|
+
} else if (os.platform() === 'android') {
|
|
381
|
+
getUserInfoFromEnv(userInfo);
|
|
382
|
+
} else {
|
|
383
|
+
// Linux/Unix systems
|
|
384
|
+
getUserInfoFromIni(userInfo);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return userInfo;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function appendUserInfo(fields) {
|
|
391
|
+
/** Append user info to fields dictionary. */
|
|
392
|
+
const userInfo = getUserInfo();
|
|
393
|
+
|
|
394
|
+
function addFieldIfNotEmpty(key, value) {
|
|
395
|
+
if (value) {
|
|
396
|
+
fields[key] = value;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
addFieldIfNotEmpty('InstanceID', userInfo.instanceID);
|
|
401
|
+
addFieldIfNotEmpty('aliUid', userInfo.AliUID);
|
|
402
|
+
addFieldIfNotEmpty('desktopId', userInfo.desktopID);
|
|
403
|
+
addFieldIfNotEmpty('desktopGroupId', userInfo.desktopGroupID);
|
|
404
|
+
addFieldIfNotEmpty('appInstanceGroupId', userInfo.appInstanceGroupID);
|
|
405
|
+
addFieldIfNotEmpty('imageVersion', userInfo.imageVersion);
|
|
406
|
+
addFieldIfNotEmpty('otaVersion', userInfo.fotaVersion);
|
|
407
|
+
addFieldIfNotEmpty('officeSiteId', userInfo.officeSiteID);
|
|
408
|
+
addFieldIfNotEmpty('osType', userInfo.osEdition);
|
|
409
|
+
addFieldIfNotEmpty('osVersion', userInfo.osVersion);
|
|
410
|
+
addFieldIfNotEmpty('osBuild', userInfo.osBuild);
|
|
411
|
+
addFieldIfNotEmpty('regionId', userInfo.regionID);
|
|
412
|
+
addFieldIfNotEmpty('appInstanceId', userInfo.appInstanceID);
|
|
413
|
+
addFieldIfNotEmpty('dsMode', userInfo.dsMode);
|
|
414
|
+
addFieldIfNotEmpty('localHostName', userInfo.localHostName);
|
|
415
|
+
|
|
416
|
+
// Handle special cases for username
|
|
417
|
+
if (['administrator', 'root', ''].includes(userInfo.userName)) {
|
|
418
|
+
userInfo.userName = getUsername();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
fields.userName = userInfo.userName;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Global variables to simulate C++ static variables
|
|
425
|
+
let _userInfo = null;
|
|
426
|
+
let _initialized = false;
|
|
427
|
+
|
|
428
|
+
function initUserInfo() {
|
|
429
|
+
/** Initialize user info. */
|
|
430
|
+
if (!_initialized) {
|
|
431
|
+
_userInfo = getUserInfo();
|
|
432
|
+
_initialized = true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function updateUserInfo() {
|
|
437
|
+
/** Update user info. */
|
|
438
|
+
_userInfo = getUserInfo();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function getUserInfoSafe() {
|
|
442
|
+
/** Get user info safely (thread-safe). */
|
|
443
|
+
if (!_initialized) {
|
|
444
|
+
initUserInfo();
|
|
445
|
+
}
|
|
446
|
+
return _userInfo;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = {
|
|
450
|
+
getDefaultObserverConfig,
|
|
451
|
+
getUserInfo,
|
|
452
|
+
appendUserInfo,
|
|
453
|
+
initUserInfo,
|
|
454
|
+
updateUserInfo,
|
|
455
|
+
getUserInfoSafe,
|
|
456
|
+
getUserInfoFromIni,
|
|
457
|
+
UserInfo
|
|
458
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"host":"","service":"unknown","resource":{"service.name":"test","env.osVersion":"macOS 25.0.0","env.userName":"panxiangpeng"},"name":"test_span","kind":"","traceID":"000000000000000000093e58102c3734","spanID":"0005dc64bdb79db1","parentSpanID":"ffffffffffffffff","links":[],"logs":[{"Name":"event1","Time":88834253599625,"attribute":{"event_attr":"event_value"}}],"traceState":"","start":88834253579.5,"end":88834258160.666,"duration":4581.166,"attribute":{"key":"value","key2":"value2"},"statusCode":"ERROR"}
|
|
2
|
+
{"host":"","service":"unknown","resource":{"service.name":"test","env.osVersion":"macOS 25.0.0","env.userName":"panxiangpeng"},"name":"test_exception","kind":"","traceID":"00000000000000000006add8749fc552","spanID":"0000f7df6181896d","parentSpanID":"","links":[],"logs":[{"Name":"event1","Time":88834258513833,"attribute":{"event_attr":"event_value"}}],"traceState":"","start":88834258511.041,"end":88834258532.125,"duration":21.084,"attribute":{"key":"value"},"statusCode":"UNSET"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"time":"2025-11-04T10:30:28.130+0800","eventName":"test_trace","module":"test","osVersion":"macOS 25.0.0","userName":"panxiangpeng"}
|
|
2
|
+
{"time":"2025-11-04T10:30:28.135+0800","eventName":"test","module":"test","osVersion":"macOS 25.0.0","userName":"panxiangpeng","properties":{"args":["/usr/local/bin/node","/Users/panxiangpeng/Work/code/wuying-guestos-observer-python/js/examples/demo.js"]}}
|
|
3
|
+
{"time":"2025-11-04T10:30:28.135+0800","eventName":"test_exception","module":"test","osVersion":"macOS 25.0.0","userName":"panxiangpeng"}
|