@aflueza/log-analysis 0.0.0-stage → 0.2.1-rc.2
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 +20 -2
- package/cordis.patch.yml +3 -0
- package/lib/analyzer.js +1093 -0
- package/lib/client.js +40766 -0
- package/lib/database/parser-rules-database.js +199 -0
- package/lib/index.js +540 -0
- package/lib/shared/types.js +1 -0
- package/lib/style.css +1872 -0
- package/package.json +65 -4
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
const SCHEMA_VERSION = 2;
|
|
6
|
+
export class ParserRulesDatabaseError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class ParserRulesDatabase {
|
|
14
|
+
db;
|
|
15
|
+
writeQueue = Promise.resolve();
|
|
16
|
+
constructor(path) {
|
|
17
|
+
const absolute = resolve(path);
|
|
18
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
19
|
+
this.db = new DatabaseSync(absolute);
|
|
20
|
+
try {
|
|
21
|
+
this.db.exec('PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;');
|
|
22
|
+
this.migrate();
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
this.db.close();
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
close() {
|
|
30
|
+
this.db.close();
|
|
31
|
+
}
|
|
32
|
+
list() {
|
|
33
|
+
return this.db.prepare('SELECT * FROM parser_rules ORDER BY name COLLATE NOCASE').all().map(mapRow);
|
|
34
|
+
}
|
|
35
|
+
get(id) {
|
|
36
|
+
const row = this.db.prepare('SELECT * FROM parser_rules WHERE id=?').get(id);
|
|
37
|
+
if (!row)
|
|
38
|
+
throw new ParserRulesDatabaseError('not-found', '解析规则不存在');
|
|
39
|
+
return mapRow(row);
|
|
40
|
+
}
|
|
41
|
+
findByName(name) {
|
|
42
|
+
const row = this.db
|
|
43
|
+
.prepare('SELECT * FROM parser_rules WHERE name=? COLLATE NOCASE')
|
|
44
|
+
.get(name.trim());
|
|
45
|
+
return row ? mapRow(row) : undefined;
|
|
46
|
+
}
|
|
47
|
+
async create(name, rule, sampleRaw = '') {
|
|
48
|
+
return this.enqueue(() => {
|
|
49
|
+
const clean = normalizeName(name);
|
|
50
|
+
const valid = validateRule(rule);
|
|
51
|
+
const sample = normalizeSample(sampleRaw);
|
|
52
|
+
if (this.findByName(clean))
|
|
53
|
+
throw new ParserRulesDatabaseError('duplicate-name', '解析规则名称已存在');
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const id = randomUUID();
|
|
56
|
+
this.db
|
|
57
|
+
.prepare('INSERT INTO parser_rules(id,name,format,record_start,fields,continuation,sample_raw,created_at,updated_at,revision) VALUES(?,?,?,?,?,?,?,?,?,1)')
|
|
58
|
+
.run(id, clean, valid.format, valid.recordStart, JSON.stringify(valid.fields), valid.continuation, sample, now, now);
|
|
59
|
+
return this.get(id);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async update(id, name, rule, sampleRaw = '') {
|
|
63
|
+
return this.enqueue(() => {
|
|
64
|
+
const current = this.get(id);
|
|
65
|
+
const clean = normalizeName(name);
|
|
66
|
+
const valid = validateRule(rule);
|
|
67
|
+
const sample = normalizeSample(sampleRaw);
|
|
68
|
+
const conflict = this.findByName(clean);
|
|
69
|
+
if (conflict && conflict.id !== id)
|
|
70
|
+
throw new ParserRulesDatabaseError('duplicate-name', '解析规则名称已存在');
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
this.db
|
|
73
|
+
.prepare('UPDATE parser_rules SET name=?,format=?,record_start=?,fields=?,continuation=?,sample_raw=?,updated_at=?,revision=? WHERE id=?')
|
|
74
|
+
.run(clean, valid.format, valid.recordStart, JSON.stringify(valid.fields), valid.continuation, sample, now, current.revision + 1, id);
|
|
75
|
+
return this.get(id);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async delete(id) {
|
|
79
|
+
return this.enqueue(() => {
|
|
80
|
+
const result = this.db.prepare('DELETE FROM parser_rules WHERE id=?').run(id);
|
|
81
|
+
if (Number(result.changes) === 0)
|
|
82
|
+
throw new ParserRulesDatabaseError('not-found', '解析规则不存在');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
enqueue(task) {
|
|
86
|
+
const run = this.writeQueue.then(task);
|
|
87
|
+
this.writeQueue = run.then(() => undefined, () => undefined);
|
|
88
|
+
return run;
|
|
89
|
+
}
|
|
90
|
+
migrate() {
|
|
91
|
+
const version = Number(this.db.prepare('PRAGMA user_version').get().user_version);
|
|
92
|
+
if (version > SCHEMA_VERSION)
|
|
93
|
+
throw new Error(`解析规则数据库版本 ${version} 不受支持`);
|
|
94
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
95
|
+
try {
|
|
96
|
+
this.db.exec("CREATE TABLE IF NOT EXISTS parser_rules (id TEXT PRIMARY KEY,name TEXT NOT NULL UNIQUE COLLATE NOCASE,format TEXT NOT NULL,record_start TEXT NOT NULL,fields TEXT NOT NULL,continuation TEXT NOT NULL,sample_raw TEXT NOT NULL DEFAULT '',created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL,revision INTEGER NOT NULL DEFAULT 1);");
|
|
97
|
+
if (version === 1)
|
|
98
|
+
this.db.exec("ALTER TABLE parser_rules ADD COLUMN sample_raw TEXT NOT NULL DEFAULT '';");
|
|
99
|
+
this.db.exec(`PRAGMA user_version=${SCHEMA_VERSION};`);
|
|
100
|
+
this.db.exec('COMMIT');
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
this.db.exec('ROLLBACK');
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function normalizeName(value) {
|
|
109
|
+
const name = value.trim();
|
|
110
|
+
if (!name)
|
|
111
|
+
throw new ParserRulesDatabaseError('invalid-name', '解析规则名称不能为空');
|
|
112
|
+
return name;
|
|
113
|
+
}
|
|
114
|
+
function normalizeSample(value) {
|
|
115
|
+
return String(value ?? '')
|
|
116
|
+
.replace(/\r\n?/g, '\n')
|
|
117
|
+
.slice(0, 120_000);
|
|
118
|
+
}
|
|
119
|
+
function validateRule(value) {
|
|
120
|
+
if (!value ||
|
|
121
|
+
value.version !== 1 ||
|
|
122
|
+
typeof value.format !== 'string' ||
|
|
123
|
+
!value.format.trim() ||
|
|
124
|
+
typeof value.recordStart !== 'string' ||
|
|
125
|
+
!value.fields ||
|
|
126
|
+
value.continuation !== 'until-next-record')
|
|
127
|
+
throw new ParserRulesDatabaseError('invalid-rule', '解析规则结构无效');
|
|
128
|
+
const fields = { ...value.fields };
|
|
129
|
+
validatePattern(value.recordStart, 'recordStart', true, false);
|
|
130
|
+
for (const [field, spec] of Object.entries(fields)) {
|
|
131
|
+
if (!spec || typeof spec.pattern !== 'string')
|
|
132
|
+
throw new ParserRulesDatabaseError('invalid-rule', `解析规则字段无效:${field}`);
|
|
133
|
+
if (!spec.pattern.trim()) {
|
|
134
|
+
delete fields[field];
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
validatePattern(spec.pattern, field, false, true);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
version: 1,
|
|
141
|
+
format: value.format.trim().slice(0, 80),
|
|
142
|
+
recordStart: value.recordStart,
|
|
143
|
+
fields,
|
|
144
|
+
continuation: 'until-next-record',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function validatePattern(pattern, label, anchored, capture) {
|
|
148
|
+
const fieldLabel = displayParserField(label);
|
|
149
|
+
if (!pattern ||
|
|
150
|
+
pattern.length > 512 ||
|
|
151
|
+
(anchored && !pattern.startsWith('^')) ||
|
|
152
|
+
/\(\?<[/=!]/.test(pattern) ||
|
|
153
|
+
/\\[1-9]|\\k</.test(pattern))
|
|
154
|
+
throw new ParserRulesDatabaseError('invalid-rule', `解析规则正则无效:${fieldLabel}`);
|
|
155
|
+
try {
|
|
156
|
+
const expression = new RegExp(pattern, 'm');
|
|
157
|
+
if (anchored && expression.test(''))
|
|
158
|
+
throw new Error();
|
|
159
|
+
if (capture && !/(^|[^\\])\((?!\?)/.test(pattern))
|
|
160
|
+
throw new Error();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
throw new ParserRulesDatabaseError('invalid-rule', `解析规则正则无效:${fieldLabel}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function displayParserField(field) {
|
|
167
|
+
const labels = {
|
|
168
|
+
recordStart: '记录起始正则',
|
|
169
|
+
level: '级别',
|
|
170
|
+
timestamp: '时间',
|
|
171
|
+
exceptionType: '异常',
|
|
172
|
+
loggerName: '类名',
|
|
173
|
+
threadName: '线程',
|
|
174
|
+
sourceLine: '行号',
|
|
175
|
+
traceId: 'TraceId',
|
|
176
|
+
spanId: 'SpanId',
|
|
177
|
+
service: '服务',
|
|
178
|
+
mdc: 'MDC',
|
|
179
|
+
message: '消息',
|
|
180
|
+
};
|
|
181
|
+
return labels[field] ?? field;
|
|
182
|
+
}
|
|
183
|
+
function mapRow(row) {
|
|
184
|
+
return {
|
|
185
|
+
id: String(row.id),
|
|
186
|
+
name: String(row.name),
|
|
187
|
+
rule: {
|
|
188
|
+
version: 1,
|
|
189
|
+
format: String(row.format),
|
|
190
|
+
recordStart: String(row.record_start),
|
|
191
|
+
fields: JSON.parse(String(row.fields)),
|
|
192
|
+
continuation: String(row.continuation),
|
|
193
|
+
},
|
|
194
|
+
sampleRaw: String(row.sample_raw ?? ''),
|
|
195
|
+
createdAt: Number(row.created_at),
|
|
196
|
+
updatedAt: Number(row.updated_at),
|
|
197
|
+
revision: Number(row.revision),
|
|
198
|
+
};
|
|
199
|
+
}
|