ECharts 日志记录方案
📋 概述
日志是调试、监控和排查问题的重要手段。在ECharts应用中,合理的日志策略能够:
- ✅ 追踪行为:记录用户交互和数据变化
- ✅ 快速定位:通过日志快速定位问题根源
- ✅ 性能分析:记录关键操作耗时
- ✅ 数据审计:追溯数据变更历史
本文档介绍ECharts应用的日志记录最佳实践。
🎯 核心概念
日志级别
typescript
enum LogLevel {
/** 调试信息 - 详细的技术细节 */
DEBUG = 0,
/** 信息 - 一般信息 */
INFO = 1,
/** 警告 - 潜在问题 */
WARN = 2,
/** 错误 - 严重问题 */
ERROR = 3,
/** 致命 - 系统崩溃 */
FATAL = 4
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
日志分类
| 类型 | 记录内容 | 示例 |
|---|---|---|
| 初始化日志 | 图表创建、配置加载 | "Chart initialized with canvas renderer" |
| 数据日志 | 数据更新、转换、验证 | "Data updated: 100 points" |
| 交互日志 | 点击、悬停、缩放 | "User clicked on data point [3, 5]" |
| 性能日志 | 渲染时间、内存占用 | "Render completed in 45ms" |
| 错误日志 | 异常、失败、降级 | "Failed to render: Invalid data" |
🔧 日志实现
1. 结构化日志系统
日志接口定义
typescript
interface LogEntry {
/** 时间戳 */
timestamp: number;
/** 日志级别 */
level: LogLevel;
/** 日志消息 */
message: string;
/** 附加数据 */
data?: any;
/** 模块名称 */
module?: string;
/** 图表ID */
chartId?: string;
/** 用户ID */
userId?: string;
}
interface LoggerConfig {
/** 最低日志级别 */
minLevel: LogLevel;
/** 是否输出到控制台 */
enableConsole: boolean;
/** 是否上报到服务器 */
enableRemote: boolean;
/** 远程上报地址 */
remoteUrl?: string;
/** 日志过滤函数 */
filter?: (entry: LogEntry) => boolean;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
日志类实现
typescript
class ChartLogger {
private config: LoggerConfig;
private buffer: LogEntry[] = [];
private readonly MAX_BUFFER_SIZE = 100;
constructor(config: LoggerConfig) {
this.config = config;
}
/**
* 记录调试日志
*/
debug(message: string, data?: any, chartId?: string): void {
this.log(LogLevel.DEBUG, message, data, chartId);
}
/**
* 记录信息日志
*/
info(message: string, data?: any, chartId?: string): void {
this.log(LogLevel.INFO, message, data, chartId);
}
/**
* 记录警告日志
*/
warn(message: string, data?: any, chartId?: string): void {
this.log(LogLevel.WARN, message, data, chartId);
}
/**
* 记录错误日志
*/
error(message: string, error?: any, chartId?: string): void {
this.log(LogLevel.ERROR, message, error, chartId);
}
/**
* 记录致命日志
*/
fatal(message: string, error?: any, chartId?: string): void {
this.log(LogLevel.FATAL, message, error, chartId);
}
/**
* 核心日志方法
*/
private log(level: LogLevel, message: string, data?: any, chartId?: string): void {
// 检查日志级别
if (level < this.config.minLevel) {
return;
}
const entry: LogEntry = {
timestamp: Date.now(),
level,
message,
data,
chartId,
module: this.getCallerModule(),
userId: this.getUserId()
};
// 应用过滤器
if (this.config.filter && !this.config.filter(entry)) {
return;
}
// 添加到缓冲区
this.buffer.push(entry);
if (this.buffer.length > this.MAX_BUFFER_SIZE) {
this.buffer.shift();
}
// 输出到控制台
if (this.config.enableConsole) {
this.outputToConsole(entry);
}
// 上报到服务器
if (this.config.enableRemote && level >= LogLevel.WARN) {
this.sendToRemote(entry);
}
}
/**
* 控制台输出
*/
private outputToConsole(entry: LogEntry): void {
const prefix = `[${this.formatTime(entry.timestamp)}] [${LogLevel[entry.level]}]`;
const message = `${prefix} ${entry.message}`;
switch (entry.level) {
case LogLevel.DEBUG:
console.debug(message, entry.data);
break;
case LogLevel.INFO:
console.info(message, entry.data);
break;
case LogLevel.WARN:
console.warn(message, entry.data);
break;
case LogLevel.ERROR:
case LogLevel.FATAL:
console.error(message, entry.data);
break;
}
}
/**
* 远程上报
*/
private async sendToRemote(entry: LogEntry): Promise<void> {
if (!this.config.remoteUrl) {
return;
}
try {
await navigator.sendBeacon(this.config.remoteUrl, JSON.stringify(entry));
} catch (error) {
console.error('Failed to send log to remote:', error);
}
}
/**
* 格式化时间
*/
private formatTime(timestamp: number): string {
const date = new Date(timestamp);
return date.toISOString();
}
/**
* 获取调用模块
*/
private getCallerModule(): string | undefined {
const error = new Error();
const stack = error.stack?.split('\n')[3];
if (stack) {
const match = stack.match(/at\s+(.+?)\s+\(/);
return match ? match[1] : undefined;
}
}
/**
* 获取用户ID
*/
private getUserId(): string | undefined {
return localStorage.getItem('userId') || undefined;
}
/**
* 获取并清空缓冲区
*/
flush(): LogEntry[] {
const logs = [...this.buffer];
this.buffer = [];
return logs;
}
}
// 创建全局日志实例
export const logger = new ChartLogger({
minLevel: process.env.NODE_ENV === 'production' ? LogLevel.WARN : LogLevel.DEBUG,
enableConsole: true,
enableRemote: true,
remoteUrl: '/api/logs'
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
2. 图表生命周期日志
初始化阶段
typescript
class ChartLifecycleLogger {
private readonly chartId: string;
constructor(chartId: string) {
this.chartId = chartId;
}
/**
* 记录初始化开始
*/
logInitStart(config: any): void {
logger.info('Chart initialization started', {
config: this.sanitizeConfig(config)
}, this.chartId);
}
/**
* 记录初始化完成
*/
logInitComplete(duration: number): void {
logger.info('Chart initialization completed', {
duration: `${duration.toFixed(2)}ms`
}, this.chartId);
}
/**
* 记录初始化失败
*/
logInitError(error: any, duration: number): void {
logger.error('Chart initialization failed', {
error: error.message,
stack: error.stack,
duration: `${duration.toFixed(2)}ms`
}, this.chartId);
}
/**
* 记录配置更新
*/
logOptionUpdate(option: EChartsOption): void {
logger.debug('Chart option updated', {
seriesCount: option.series?.length || 0,
hasAnimation: option.animation
}, this.chartId);
}
/**
* 记录销毁
*/
logDestroy(): void {
logger.info('Chart destroyed', {}, this.chartId);
}
/**
* 清理敏感配置
*/
private sanitizeConfig(config: any): any {
const sanitized = { ...config };
// 移除敏感字段
delete sanitized.apiKey;
delete sanitized.token;
return sanitized;
}
}
// 使用示例
function useECharts(option: EChartsOption) {
const lifecycleLogger = useRef<ChartLifecycleLogger | null>(null);
useEffect(() => {
const startTime = performance.now();
// 创建生命周期日志器
lifecycleLogger.current = new ChartLifecycleLogger(`chart_${Date.now()}`);
lifecycleLogger.current.logInitStart(option);
// 初始化图表
const chart = echarts.init(container);
chart.setOption(option);
// 记录完成
const duration = performance.now() - startTime;
lifecycleLogger.current.logInitComplete(duration);
return () => {
lifecycleLogger.current?.logDestroy();
chart.dispose();
};
}, []);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
3. 数据变更日志
数据追踪器
typescript
class DataChangeTracker {
private previousData: Map<string, any> = new Map();
private readonly MAX_HISTORY = 50;
private history: Array<{
timestamp: number;
chartId: string;
change: any;
}> = [];
/**
* 记录数据变更
*/
recordChange(chartId: string, oldData: any, newData: any, reason: string): void {
const change = {
oldData: this.serialize(oldData),
newData: this.serialize(newData),
reason,
diff: this.calculateDiff(oldData, newData)
};
// 记录日志
logger.info(`Data changed in ${chartId}`, {
reason,
changes: change.diff
}, chartId);
// 保存历史
this.history.push({
timestamp: Date.now(),
chartId,
change
});
if (this.history.length > this.MAX_HISTORY) {
this.history.shift();
}
// 保存旧数据用于下次比较
this.previousData.set(chartId, oldData);
}
/**
* 获取变更历史
*/
getHistory(chartId?: string): Array<any> {
if (chartId) {
return this.history.filter(h => h.chartId === chartId);
}
return this.history;
}
/**
* 计算差异
*/
private calculateDiff(oldData: any, newData: any): any {
const diff: any = {};
if (Array.isArray(oldData) && Array.isArray(newData)) {
diff.added = newData.length - oldData.length;
diff.changed = oldData.filter((v, i) => v !== newData[i]).length;
} else {
diff.typeChanged = typeof oldData !== typeof newData;
}
return diff;
}
/**
* 序列化数据(用于比较)
*/
private serialize(data: any): string {
try {
return JSON.stringify(data);
} catch (error) {
return String(data);
}
}
}
export const dataTracker = new DataChangeTracker();
// 使用示例
function updateChartData(chartId: string, newData: number[]) {
const oldData = currentData[chartId];
// 更新数据
currentData[chartId] = newData;
// 记录变更
dataTracker.recordChange(chartId, oldData, newData, 'User refresh');
// 更新图表
chart.setOption({ series: [{ data: newData }] });
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
4. 用户交互日志
交互追踪
typescript
interface InteractionEvent {
type: 'click' | 'hover' | 'zoom' | 'brush' | 'legendToggle';
chartId: string;
target?: string;
value?: any;
timestamp: number;
}
class InteractionLogger {
private events: InteractionEvent[] = [];
/**
* 记录点击事件
*/
logClick(chartId: string, params: echarts.ECElementEvent): void {
const event: InteractionEvent = {
type: 'click',
chartId,
target: params.seriesName || params.name,
value: params.value,
timestamp: Date.now()
};
this.logInteraction(event);
}
/**
* 记录悬停事件
*/
logHover(chartId: string, params: echarts.ECElementEvent): void {
const event: InteractionEvent = {
type: 'hover',
chartId,
target: params.seriesName || params.name,
timestamp: Date.now()
};
this.logInteraction(event);
}
/**
* 记录缩放事件
*/
logZoom(chartId: string, start: number, end: number): void {
const event: InteractionEvent = {
type: 'zoom',
chartId,
value: { start, end },
timestamp: Date.now()
};
this.logInteraction(event);
}
/**
* 记录图例切换
*/
logLegendToggle(chartId: string, legendName: string, visible: boolean): void {
const event: InteractionEvent = {
type: 'legendToggle',
chartId,
target: legendName,
value: { visible },
timestamp: Date.now()
};
this.logInteraction(event);
}
/**
* 记录交互
*/
private logInteraction(event: InteractionEvent): void {
logger.debug(`Interaction: ${event.type}`, event, event.chartId);
// 保存到缓冲区
this.events.push(event);
// 定期上报
if (this.events.length >= 10) {
this.flushEvents();
}
}
/**
* 上报事件
*/
private flushEvents(): void {
const events = [...this.events];
this.events = [];
// 发送到分析平台
fetch('/api/analytics/interactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(events)
}).catch(error => {
console.error('Failed to send interaction events:', error);
});
}
/**
* 获取最近事件
*/
getRecentEvents(count = 10): InteractionEvent[] {
return this.events.slice(-count);
}
}
export const interactionLogger = new InteractionLogger();
// 使用示例
chart.on('click', (params) => {
interactionLogger.logClick('my_chart', params);
// 业务逻辑...
});
chart.on('mouseover', (params) => {
interactionLogger.logHover('my_chart', params);
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
5. 性能日志
性能指标收集
typescript
class PerformanceLogger {
private metrics: Map<string, number[]> = new Map();
/**
* 记录渲染时间
*/
logRenderTime(chartId: string, duration: number): void {
this.recordMetric(`${chartId}.render`, duration);
if (duration > 1000) {
logger.warn(`Slow render detected: ${duration.toFixed(2)}ms`, {
chartId,
duration
}, chartId);
}
}
/**
* 记录数据加载时间
*/
logLoadTime(chartId: string, duration: number): void {
this.recordMetric(`${chartId}.load`, duration);
logger.info(`Data loaded in ${duration.toFixed(2)}ms`, { chartId }, chartId);
}
/**
* 记录resize时间
*/
logResizeTime(chartId: string, duration: number): void {
this.recordMetric(`${chartId}.resize`, duration);
}
/**
* 记录内存使用
*/
logMemoryUsage(chartId: string, memoryMB: number): void {
this.recordMetric(`${chartId}.memory`, memoryMB);
if (memoryMB > 500) {
logger.warn(`High memory usage: ${memoryMB.toFixed(2)}MB`, {
chartId,
memoryMB
}, chartId);
}
}
/**
* 记录指标
*/
private recordMetric(key: string, value: number): void {
if (!this.metrics.has(key)) {
this.metrics.set(key, []);
}
const values = this.metrics.get(key)!;
values.push(value);
// 限制历史记录长度
if (values.length > 100) {
values.shift();
}
}
/**
* 获取统计信息
*/
getStats(key: string): {
avg: number;
min: number;
max: number;
count: number;
} | null {
const values = this.metrics.get(key);
if (!values || values.length === 0) {
return null;
}
return {
avg: values.reduce((a, b) => a + b, 0) / values.length,
min: Math.min(...values),
max: Math.max(...values),
count: values.length
};
}
/**
* 导出所有指标
*/
exportMetrics(): Record<string, any> {
const result: Record<string, any> = {};
this.metrics.forEach((values, key) => {
result[key] = this.getStats(key);
});
return result;
}
}
export const perfLogger = new PerformanceLogger();
// 使用示例
const startTime = performance.now();
chart.setOption(option);
const renderTime = performance.now() - startTime;
perfLogger.logRenderTime('my_chart', renderTime);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
💡 实战案例
案例1:Redux中间件日志
typescript
import { Middleware } from '@reduxjs/toolkit';
/**
* Redux中间件 - 记录图表相关action
*/
export const chartLoggerMiddleware: Middleware = store => next => action => {
// 只记录图表相关的action
if (action.type.startsWith('charts/')) {
logger.debug('Action dispatched', {
type: action.type,
payload: action.payload
});
// 记录性能
const startTime = performance.now();
const result = next(action);
const duration = performance.now() - startTime;
logger.debug(`Action processed in ${duration.toFixed(2)}ms`, {
type: action.type
});
return result;
}
return next(action);
};
// 使用
const store = configureStore({
reducer: { charts: chartsReducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(chartLoggerMiddleware)
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
案例2:生产环境日志配置
typescript
/**
* 根据环境配置日志策略
*/
export function createLoggerForEnvironment(): ChartLogger {
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
return new ChartLogger({
// 开发环境显示所有日志,生产环境只显示警告以上
minLevel: isDevelopment ? LogLevel.DEBUG : LogLevel.WARN,
// 开发环境启用控制台
enableConsole: isDevelopment,
// 生产环境启用远程上报
enableRemote: isProduction,
// 远程上报地址
remoteUrl: '/api/logs',
// 过滤掉调试日志
filter: (entry) => {
// 生产环境不过滤
if (isProduction) return true;
// 开发环境过滤掉高频的debug日志
if (entry.level === LogLevel.DEBUG && entry.message.includes('mousemove')) {
return false;
}
return true;
}
});
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
⚠️ 常见问题
问题1:日志过多影响性能
症状:开启日志后页面变卡
原因:频繁的控制台输出和对象序列化
解决:
typescript
// ❌ 错误做法 - 即使不输出也会执行
logger.debug('Mouse position', { x: e.clientX, y: e.clientY });
// ✅ 正确做法 - 先检查级别
if (logger.isLevelEnabled(LogLevel.DEBUG)) {
logger.debug('Mouse position', { x: e.clientX, y: e.clientY });
}1
2
3
4
5
6
7
2
3
4
5
6
7
问题2:敏感信息泄露
症状:日志中包含API密钥、用户隐私
解决:
typescript
// 脱敏处理
function sanitize(obj: any): any {
const sensitive = ['password', 'token', 'apiKey', 'secret'];
const sanitized = { ...obj };
sensitive.forEach(key => {
if (key in sanitized) {
sanitized[key] = '***';
}
});
return sanitized;
}
logger.info('API response', sanitize(response));1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
🎯 最佳实践
1. 日志级别选择
| 场景 | 级别 | 示例 |
|---|---|---|
| 函数入口/出口 | DEBUG | "Starting data processing" |
| 重要操作 | INFO | "Chart initialized" |
| 异常情况 | WARN | "Data contains NaN values" |
| 功能失败 | ERROR | "Failed to load data" |
| 系统崩溃 | FATAL | "Out of memory" |
2. 结构化日志格式
json
{
"timestamp": 1234567890,
"level": "ERROR",
"message": "Chart render failed",
"chartId": "chart_001",
"data": {
"error": "Invalid option",
"seriesType": "line"
},
"userId": "user_123"
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
3. 日志轮转策略
- 单个文件最大10MB
- 保留最近7天
- 超过大小自动归档
📊 日志性能开销
| 操作 | 耗时 | 说明 |
|---|---|---|
| 控制台输出 | 1-5ms | 取决于浏览器 |
| 远程上报 | 10-50ms | 使用sendBeacon异步 |
| 数据序列化 | 0.1-1ms | JSON.stringify |
| 级别检查 | < 0.01ms | 数值比较 |
🔗 相关链接
💎 总结
合理的日志策略对于ECharts应用的开发和运维至关重要。遵循以下原则:
- 分级记录:根据重要性选择合适的日志级别
- 结构化:使用统一的格式和字段
- 性能优先:避免在生产环境输出过多日志
- 安全第一:脱敏敏感信息
- 可检索:添加足够的上下文便于搜索和分析
