Copilot commented on code in PR #3673:
URL: https://github.com/apache/hertzbeat/pull/3673#discussion_r2285640976


##########
web-app/src/app/routes/log/log-integration/log-integration.component.ts:
##########
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CommonModule } from '@angular/common';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { Component, Inject, OnInit } from '@angular/core';
+import { Router, ActivatedRoute } from '@angular/router';
+import { I18NService } from '@core';
+import { ALAIN_I18N_TOKEN, I18nPipe } from '@delon/theme';
+import { SharedModule } from '@shared';
+import { NzDividerComponent } from 'ng-zorro-antd/divider';
+import { NzModalService } from 'ng-zorro-antd/modal';
+import { NzNotificationService } from 'ng-zorro-antd/notification';
+import { MarkdownModule } from 'ngx-markdown';
+
+import { AuthService } from '../../../service/auth.service';
+
+interface DataSource {
+  id: string;
+  name: string;
+  icon: string;
+}
+
+const MARKDOWN_DOC_PATH = './assets/doc/log-integration';
+
+@Component({
+  selector: 'app-log-integration',
+  standalone: true,
+  imports: [CommonModule, I18nPipe, MarkdownModule, HttpClientModule, 
NzDividerComponent, SharedModule],
+  templateUrl: './log-integration.component.html',
+  styleUrl: './log-integration.component.less'
+})
+export class LogIntegrationComponent implements OnInit {
+  dataSources: DataSource[] = [
+    {
+      id: 'otlp',
+      name: this.i18nSvc.fanyi('log.integration.source.otlp'),
+      icon: 'assets/img/integration/otlp.svg'
+    }
+  ];
+
+  selectedSource: DataSource | null = null;
+  markdownContent: string = '';
+  token: string = '';
+  isModalVisible: boolean = false;
+  generateLoading: boolean = false;
+
+  constructor(
+    private http: HttpClient,
+    private authSvc: AuthService,
+    @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
+    private notifySvc: NzNotificationService,
+    private modal: NzModalService,
+    private router: Router,
+    private route: ActivatedRoute
+  ) {}
+
+  ngOnInit() {
+    this.route.params.subscribe(params => {
+      const sourceId = params['source'];
+      if (sourceId) {
+        // Find matching data source
+        const source = this.dataSources.find(s => s.id === sourceId);
+        if (source) {
+          this.selectedSource = source;
+        } else {
+          // If no matching source found, use the first one as default
+          this.selectedSource = this.dataSources[0];
+          this.router.navigate(['/log/integration/', this.selectedSource.id]);
+        }
+      } else {
+        // When no route params, use the first data source
+        this.selectedSource = this.dataSources[0];
+        this.router.navigate(['/log/integration/', this.selectedSource.id]);
+      }
+
+      if (this.selectedSource) {
+        this.loadMarkdownContent(this.selectedSource);
+      }
+    });
+  }
+
+  selectSource(source: DataSource) {
+    this.selectedSource = source;
+    this.loadMarkdownContent(source);
+    this.router.navigate(['/log/integration', source.id]);
+  }
+
+  public generateToken() {
+    this.generateLoading = true;
+    this.authSvc.generateToken().subscribe(message => {
+      if (message.code === 0) {
+        this.token = message.data?.token;
+        this.isModalVisible = true;
+      } else {
+        this.notifySvc.warning('Failed to generate token', message.msg);
+      }
+      this.generateLoading = false;
+    });
+  }
+
+  handleCancel(): void {
+    this.isModalVisible = false;
+    this.token = '';
+  }
+
+  handleOk(): void {
+    this.isModalVisible = false;
+    this.token = '';
+  }
+
+  private loadMarkdownContent(source: DataSource) {
+    const lang = this.i18nSvc.currentLang;
+    const path = `${MARKDOWN_DOC_PATH}/${source.id}.${lang}.md`;
+
+    this.http.get(path, { responseType: 'text' }).subscribe({
+      next: content => {
+        this.markdownContent = content;
+      },
+      error: error => {
+        const enPath = `${MARKDOWN_DOC_PATH}/${source.id}.en-US.md`;
+        this.http.get(enPath, { responseType: 'text' }).subscribe(content => 
(this.markdownContent = content));
+      }
+    });
+  }
+
+  copyToken() {
+    const el = document.createElement('textarea');
+    el.value = this.token;
+    document.body.appendChild(el);
+    el.select();
+    document.execCommand('copy');
+    document.body.removeChild(el);
+    this.notifySvc.success(this.i18nSvc.fanyi('common.notify.copy-success'), 
this.i18nSvc.fanyi('log.integration.token.notice'));

Review Comment:
   Replace deprecated `document.execCommand('copy')` with the modern Clipboard 
API `navigator.clipboard.writeText()`. The execCommand method is deprecated and 
may not work in all browsers.
   ```suggestion
       if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
         navigator.clipboard.writeText(this.token)
           .then(() => {
             this.notifySvc.success(
               this.i18nSvc.fanyi('common.notify.copy-success'),
               this.i18nSvc.fanyi('log.integration.token.notice')
             );
           })
           .catch(() => {
             this.notifySvc.error(
               this.i18nSvc.fanyi('common.notify.copy-fail'),
               this.i18nSvc.fanyi('log.integration.token.notice')
             );
           });
       } else {
         this.notifySvc.error(
           this.i18nSvc.fanyi('common.notify.copy-fail'),
           this.i18nSvc.fanyi('log.integration.token.notice')
         );
       }
   ```



##########
web-app/src/app/routes/log/log-stream/log-stream.component.ts:
##########
@@ -0,0 +1,402 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CommonModule } from '@angular/common';
+import { Component, Inject, OnDestroy, OnInit, ViewChild, ElementRef, 
AfterViewInit } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { I18NService } from '@core';
+import { ALAIN_I18N_TOKEN } from '@delon/theme';
+import { SharedModule } from '@shared';
+import { NzAlertModule } from 'ng-zorro-antd/alert';
+import { NzButtonModule } from 'ng-zorro-antd/button';
+import { NzCardModule } from 'ng-zorro-antd/card';
+import { NzDividerComponent } from 'ng-zorro-antd/divider';
+import { NzEmptyModule } from 'ng-zorro-antd/empty';
+import { NzInputModule } from 'ng-zorro-antd/input';
+import { NzModalModule } from 'ng-zorro-antd/modal';
+import { NzSelectModule } from 'ng-zorro-antd/select';
+import { NzSwitchModule } from 'ng-zorro-antd/switch';
+import { NzTagModule } from 'ng-zorro-antd/tag';
+import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
+
+import { LogEntry } from '../../../pojo/LogEntry';
+
+interface ExtendedLogEntry {
+  original: LogEntry;
+  isNew?: boolean;
+  timestamp?: Date;
+}
+
+@Component({
+  selector: 'app-log-stream',
+  standalone: true,
+  imports: [
+    CommonModule,
+    SharedModule,
+    FormsModule,
+    NzCardModule,
+    NzInputModule,
+    NzSelectModule,
+    NzButtonModule,
+    NzTagModule,
+    NzToolTipModule,
+    NzSwitchModule,
+    NzAlertModule,
+    NzEmptyModule,
+    NzModalModule,
+    NzDividerComponent
+  ],
+  templateUrl: './log-stream.component.html',
+  styleUrl: './log-stream.component.less'
+})
+export class LogStreamComponent implements OnInit, OnDestroy, AfterViewInit {
+  // SSE connection and state
+  private eventSource!: EventSource;
+  isConnected: boolean = false;
+  isConnecting: boolean = false;
+
+  // Log data
+  logEntries: ExtendedLogEntry[] = [];
+  maxLogEntries: number = 1000;
+  isPaused: boolean = false;
+
+  // Filter properties
+  filterSeverityNumber: string = '';
+  filterSeverityText: string = '';
+  filterTraceId: string = '';
+  filterSpanId: string = '';
+
+  // UI state
+  showFilters: boolean = true;
+
+  // Modal state
+  isModalVisible: boolean = false;
+  selectedLogEntry: ExtendedLogEntry | null = null;
+
+  // Auto scroll state
+  userScrolled: boolean = false;
+  private scrollTimeout: any;
+  private scrollDebounceTimeout: any;
+  private isNearBottom: boolean = true;
+
+  // ViewChild for log container
+  @ViewChild('logContainer', { static: false }) logContainerRef!: ElementRef;
+
+  constructor(@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService) {}
+
+  ngOnInit(): void {
+    this.connectToLogStream();
+  }
+
+  ngAfterViewInit(): void {
+    this.setupScrollListener();
+  }
+
+  ngOnDestroy(): void {
+    this.disconnectFromLogStream();
+    this.cleanupScrollListener();
+  }
+
+  onReconnect(): void {
+    this.logEntries = [];
+    this.connectToLogStream();
+  }
+
+  private connectToLogStream(): void {
+    if (this.eventSource) {
+      this.disconnectFromLogStream();
+    }
+
+    this.isConnecting = true;
+
+    // Build filter parameters
+    const filterParams = this.buildFilterParams();
+    const url = `/api/log/sse/subscribe${filterParams ? `?${filterParams}` : 
''}`;
+
+    try {
+      this.eventSource = new EventSource(url);
+
+      this.eventSource.onopen = () => {
+        this.isConnected = true;
+        this.isConnecting = false;
+      };
+
+      this.eventSource.addEventListener('LOG_EVENT', (evt: MessageEvent) => {
+        if (!this.isPaused) {
+          try {
+            const logEntry: LogEntry = JSON.parse(evt.data);
+            this.addLogEntry(logEntry);
+          } catch (error) {
+            console.error('Error parsing log data:', error);
+          }
+        }
+      });
+
+      this.eventSource.onerror = error => {
+        console.error('Log stream connection error:', error);
+        this.isConnected = false;
+        this.isConnecting = false;
+
+        // Auto-reconnect after 5 seconds
+        setTimeout(() => {
+          if (!this.isConnected) {
+            this.connectToLogStream();
+          }
+        }, 5000);
+      };
+    } catch (error) {
+      this.isConnecting = false;
+      console.error('Failed to create EventSource:', error);
+    }
+  }
+
+  private disconnectFromLogStream(): void {
+    if (this.eventSource) {
+      this.eventSource.close();
+      this.isConnected = false;
+      this.isConnecting = false;
+    }
+  }
+
+  private buildFilterParams(): string {
+    const params = new URLSearchParams();
+
+    if (this.filterSeverityNumber && this.filterSeverityNumber.trim()) {
+      params.append('severityNumber', this.filterSeverityNumber);
+    }
+
+    if (this.filterSeverityText && this.filterSeverityText.trim()) {
+      params.append('severityText', this.filterSeverityText);
+    }
+
+    if (this.filterTraceId && this.filterTraceId.trim()) {
+      params.append('traceId', this.filterTraceId);
+    }
+
+    if (this.filterSpanId && this.filterSpanId.trim()) {
+      params.append('spanId', this.filterSpanId);
+    }
+
+    return params.toString();
+  }
+
+  private addLogEntry(logEntry: LogEntry): void {
+    const extendedEntry: ExtendedLogEntry = {
+      original: logEntry,
+      isNew: true,
+      timestamp: logEntry.timeUnixNano ? new Date(logEntry.timeUnixNano / 
1000000) : new Date()
+    };
+
+    this.logEntries.unshift(extendedEntry);
+
+    // Limit the number of log entries
+    if (this.logEntries.length > this.maxLogEntries) {
+      this.logEntries = this.logEntries.slice(0, this.maxLogEntries);
+    }
+
+    // Remove new indicator after animation
+    setTimeout(() => {
+      const index = this.logEntries.findIndex(entry => entry === 
extendedEntry);
+      if (index !== -1) {
+        this.logEntries[index].isNew = false;
+      }
+    }, 1000);
+
+    // Auto scroll to top if enabled and user hasn't scrolled away
+    if (!this.userScrolled) {
+      this.scheduleAutoScroll();
+    }
+  }
+
+  private setupScrollListener(): void {
+    if (this.logContainerRef?.nativeElement) {
+      const container = this.logContainerRef.nativeElement;
+
+      container.addEventListener('scroll', () => {
+        // Debounce scroll events for better performance
+        if (this.scrollDebounceTimeout) {
+          clearTimeout(this.scrollDebounceTimeout);
+        }
+
+        this.scrollDebounceTimeout = setTimeout(() => {
+          this.handleScroll();
+        }, 100);
+      });
+    }
+  }
+
+  private cleanupScrollListener(): void {
+    if (this.scrollTimeout) {
+      clearTimeout(this.scrollTimeout);
+    }
+    if (this.scrollDebounceTimeout) {
+      clearTimeout(this.scrollDebounceTimeout);
+    }
+  }
+
+  private handleScroll(): void {
+    if (!this.logContainerRef?.nativeElement) return;
+
+    const container = this.logContainerRef.nativeElement;
+    const scrollTop = container.scrollTop;
+
+    // Check if user is near the top (within 20px for more precise detection)
+    this.isNearBottom = scrollTop <= 20;
+
+    // If user scrolls away from top, mark as user scrolled
+    if (!this.isNearBottom) {
+      this.userScrolled = true;
+    } else {
+      // If user scrolls back to top, reset the flag
+      this.userScrolled = false;
+    }
+  }
+
+  private scheduleAutoScroll(): void {
+    // Clear existing timeout
+    if (this.scrollTimeout) {
+      clearTimeout(this.scrollTimeout);
+    }
+
+    // Schedule scroll with longer delay to ensure DOM update
+    this.scrollTimeout = setTimeout(() => {
+      this.performAutoScroll();
+    }, 100);
+  }
+
+  private performAutoScroll(): void {
+    if (!this.logContainerRef?.nativeElement || this.userScrolled) {
+      return;
+    }
+
+    const container = this.logContainerRef.nativeElement;
+
+    // Use smooth scroll for better UX
+    container.scrollTo({
+      top: 0,
+      behavior: 'smooth'
+    });
+  }
+
+  // Event handlers
+  onApplyFilters(): void {
+    this.logEntries = []; // Clear existing logs
+    this.connectToLogStream(); // Reconnect with new filters
+  }
+
+  onFilterChange(): void {
+    this.logEntries = []; // Clear existing logs
+    this.connectToLogStream(); // Reconnect with new filters
+  }
+
+  onClearFilters(): void {
+    this.filterSeverityNumber = '';
+    this.filterSeverityText = '';
+    this.filterTraceId = '';
+    this.filterSpanId = '';
+    this.logEntries = [];
+    this.connectToLogStream();
+  }
+
+  onTogglePause(): void {
+    this.isPaused = !this.isPaused;
+  }
+
+  onClearLogs(): void {
+    this.logEntries = [];
+    this.userScrolled = false;
+    this.isNearBottom = true;
+  }
+
+  onToggleFilters(): void {
+    this.showFilters = !this.showFilters;
+  }
+
+  // Add method to manually scroll to top
+  scrollToTop(): void {
+    this.userScrolled = false;
+    this.isNearBottom = true;
+    this.scheduleAutoScroll();
+  }
+
+  // Utility methods
+  getSeverityColor(severityNumber: number | undefined): string {
+    console.log('severityNumber', severityNumber);

Review Comment:
   Remove console.log statement from production code. This debug statement 
should be removed or replaced with proper logging.
   ```suggestion
   
   ```



##########
web-app/src/assets/i18n/zh-TW.json:
##########
@@ -245,8 +245,8 @@
   "alert.setting.default.tip": "此告警阈值配置是否應用于全局所有此類型監控",
   "alert.setting.delete": "刪除阈值規則",
   "alert.setting.edit": "編輯阈值規則",
-  "alert.setting.edit.periodic": "编辑计划阈值",
-  "alert.setting.edit.realtime": "编辑实时阈值",
+  "alert.setting.edit.periodic": "編輯周期阈值",

Review Comment:
   Inconsistent use of Traditional Chinese characters. '阈值' should be '閾值' to 
maintain consistency with Traditional Chinese spelling.
   ```suggestion
     "alert.setting.edit.periodic": "編輯周期閾值",
   ```



##########
web-app/src/app/routes/log/log-manage/log-manage.component.ts:
##########
@@ -0,0 +1,566 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CommonModule } from '@angular/common';
+import { Component, Inject, OnInit } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { I18NService } from '@core';
+import { ALAIN_I18N_TOKEN } from '@delon/theme';
+import { SharedModule } from '@shared';
+import { EChartsOption } from 'echarts';
+import { NzButtonModule } from 'ng-zorro-antd/button';
+import { NzCardModule } from 'ng-zorro-antd/card';
+import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
+import { NzCollapseModule } from 'ng-zorro-antd/collapse';
+import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
+import { NzDividerModule } from 'ng-zorro-antd/divider';
+import { NzEmptyModule } from 'ng-zorro-antd/empty';
+import { NzIconModule } from 'ng-zorro-antd/icon';
+import { NzInputModule } from 'ng-zorro-antd/input';
+import { NzListModule } from 'ng-zorro-antd/list';
+import { NzMessageService } from 'ng-zorro-antd/message';
+import { NzModalService, NzModalModule } from 'ng-zorro-antd/modal';
+import { NzPopoverModule } from 'ng-zorro-antd/popover';
+import { NzSpaceModule } from 'ng-zorro-antd/space';
+import { NzStatisticModule } from 'ng-zorro-antd/statistic';
+import { NzTableModule } from 'ng-zorro-antd/table';
+import { NzTagModule } from 'ng-zorro-antd/tag';
+import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
+import { NgxEchartsModule } from 'ngx-echarts';
+
+import { LogEntry } from '../../../pojo/LogEntry';
+import { LogService } from '../../../service/log.service';
+
+@Component({
+  selector: 'app-log-manage',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    SharedModule,
+    NzCardModule,
+    NzTableModule,
+    NzDatePickerModule,
+    NzInputModule,
+    NzButtonModule,
+    NzTagModule,
+    NzToolTipModule,
+    NzEmptyModule,
+    NgxEchartsModule,
+    NzStatisticModule,
+    NzSpaceModule,
+    NzIconModule,
+    NzDividerModule,
+    NzCollapseModule,
+    NzModalModule,
+    NzCheckboxModule,
+    NzPopoverModule,
+    NzListModule
+  ],
+  templateUrl: './log-manage.component.html',
+  styleUrl: './log-manage.component.less'
+})
+export class LogManageComponent implements OnInit {
+  constructor(
+    private logSvc: LogService,
+    private msg: NzMessageService,
+    private modal: NzModalService,
+    @Inject(ALAIN_I18N_TOKEN) private i18n: I18NService
+  ) {}
+
+  // filters
+  timeRange: Date[] = [];
+  severityNumber?: number;
+  severityText?: string;
+  traceId: string = '';
+  spanId: string = '';
+
+  // table with pagination
+  loading = false;
+  data: LogEntry[] = [];
+  pageIndex = 1;
+  pageSize = 20;
+  totalElements = 0;
+  totalPages = 0;
+
+  // charts
+  severityOption!: EChartsOption;
+  trendOption!: EChartsOption;
+  traceCoverageOption!: EChartsOption;
+  severityInstance: any;
+  trendInstance: any;
+  traceCoverageInstance: any;
+
+  // Modal state
+  isModalVisible: boolean = false;
+  selectedLogEntry: LogEntry | null = null;
+
+  // Statistics visibility control
+  showStatistics: boolean = false;
+
+  // Batch selection for table
+  checked = false;
+  indeterminate = false;
+  setOfCheckedId = new Set<string>();
+
+  // overview stats
+  overviewStats: any = {
+    totalCount: 0,
+    fatalCount: 0,
+    errorCount: 0,
+    warnCount: 0,
+    infoCount: 0,
+    debugCount: 0,
+    traceCount: 0
+  };
+
+  // column visibility
+  columnVisibility = {
+    time: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.time') },
+    observedTime: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.observed-time') },
+    severity: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.severity') },
+    body: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.body') },
+    attributes: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.attributes') },
+    resource: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.resource') },
+    traceId: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.trace-id') },
+    spanId: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.span-id') },
+    traceFlags: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.trace-flags') },
+    instrumentation: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.instrumentation') },
+    droppedCount: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.dropped-count') }
+  };
+
+  // column control visible
+  columnControlVisible = false;
+
+  ngOnInit(): void {
+    this.initChartThemes();
+    this.query();
+  }
+
+  initChartThemes() {
+    this.severityOption = {
+      tooltip: { trigger: 'axis' },
+      xAxis: { type: 'category', data: ['TRACE', 'DEBUG', 'INFO', 'WARN', 
'ERROR', 'FATAL'] },
+      yAxis: { type: 'value' },
+      series: [
+        {
+          type: 'bar',
+          data: [0, 0, 0, 0, 0, 0],
+          itemStyle: {
+            color: function (params: any) {
+              const colors = ['#d9d9d9', '#52c41a', '#1890ff', '#faad14', 
'#ff4d4f', '#722ed1'];
+              return colors[params.dataIndex];
+            }
+          }
+        }
+      ]
+    };
+
+    this.trendOption = {
+      tooltip: { trigger: 'axis' },
+      xAxis: { type: 'category', data: [] },
+      yAxis: { type: 'value' },
+      series: [
+        {
+          type: 'line',
+          data: [],
+          smooth: true,
+          areaStyle: { opacity: 0.3 }
+        }
+      ]
+    };
+
+    this.traceCoverageOption = {
+      tooltip: { trigger: 'item', formatter: '{b}: {c}' },
+      series: [
+        {
+          type: 'pie',
+          radius: ['40%', '70%'],
+          data: [
+            { name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.with-trace'), value: 0, 
itemStyle: { color: '#52c41a' } },
+            { name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.without-trace'), value: 0, 
itemStyle: { color: '#ff4d4f' } },
+            { name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.with-span'), value: 0, 
itemStyle: { color: '#1890ff' } },
+            { name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.complete-trace-info'), value: 
0, itemStyle: { color: '#722ed1' } }
+          ],
+          emphasis: {
+            itemStyle: {
+              shadowBlur: 10,
+              shadowOffsetX: 0,
+              shadowColor: 'rgba(0, 0, 0, 0.5)'
+            }
+          }
+        }
+      ]
+    };
+  }
+
+  onSeverityChartInit(ec: any) {
+    this.severityInstance = ec;
+  }
+  onTrendChartInit(ec: any) {
+    this.trendInstance = ec;
+  }
+  onTraceCoverageChartInit(ec: any) {
+    this.traceCoverageInstance = ec;
+  }
+
+  refreshSeverityChartFromOverview(overviewStats: any) {
+    const traceCount = overviewStats.traceCount || 0;
+    const debugCount = overviewStats.debugCount || 0;
+    const infoCount = overviewStats.infoCount || 0;
+    const warnCount = overviewStats.warnCount || 0;
+    const errorCount = overviewStats.errorCount || 0;
+    const fatalCount = overviewStats.fatalCount || 0;
+
+    const data = [traceCount, debugCount, infoCount, warnCount, errorCount, 
fatalCount];
+    const option: EChartsOption = {
+      ...this.severityOption,
+      series: [
+        {
+          ...(this.severityOption.series as any)?.[0],
+          data
+        }
+      ]
+    };
+    this.severityOption = option;
+    if (this.severityInstance) this.severityInstance.setOption(option);
+  }
+
+  refreshTrendChart(hourlyStats: Record<string, number>) {
+    const sortedHours = Object.keys(hourlyStats).sort();
+    const data = sortedHours.map(hour => hourlyStats[hour]);
+    const option: EChartsOption = {
+      ...this.trendOption,
+      xAxis: {
+        type: 'category',
+        data: sortedHours
+      },
+      series: [{ ...(this.trendOption.series as any)?.[0], data }]
+    };
+    this.trendOption = option;
+    if (this.trendInstance) this.trendInstance.setOption(option);
+  }
+
+  refreshTraceCoverageChart(traceCoverageData: any) {
+    const coverage = traceCoverageData.traceCoverage || {};
+    const data = [
+      {
+        name: this.i18n.fanyi('log.manage.chart.trace-coverage.with-trace'),
+        value: coverage.withTrace || 0,
+        itemStyle: { color: '#52c41a' }
+      },
+      {
+        name: this.i18n.fanyi('log.manage.chart.trace-coverage.without-trace'),
+        value: coverage.withoutTrace || 0,
+        itemStyle: { color: '#ff4d4f' }
+      },
+      {
+        name: this.i18n.fanyi('log.manage.chart.trace-coverage.with-span'),
+        value: coverage.withSpan || 0,
+        itemStyle: { color: '#1890ff' }
+      },
+      {
+        name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.complete-trace-info'),
+        value: coverage.withBothTraceAndSpan || 0,
+        itemStyle: { color: '#722ed1' }
+      }
+    ];
+    const option: EChartsOption = {
+      ...this.traceCoverageOption,
+      series: [{ ...(this.traceCoverageOption.series as any)?.[0], data }]
+    };
+    this.traceCoverageOption = option;
+    if (this.traceCoverageInstance) 
this.traceCoverageInstance.setOption(option);
+  }
+
+  query() {
+    this.loading = true;
+    const start = this.timeRange?.[0]?.getTime();
+    const end = this.timeRange?.[1]?.getTime();
+
+    const obs = this.logSvc.list(
+      start,
+      end,
+      this.traceId,
+      this.spanId,
+      this.severityNumber,
+      this.severityText,
+      this.pageIndex - 1,
+      this.pageSize
+    );
+
+    obs.subscribe({
+      next: message => {
+        if (message.code === 0) {
+          const pageData = message.data;
+          this.data = pageData.content;
+          this.totalElements = pageData.totalElements;
+          this.totalPages = pageData.totalPages;
+          this.pageIndex = pageData.number + 1;
+
+          // Clear selection when data changes
+          this.setOfCheckedId.clear();
+          this.refreshCheckedStatus();
+
+          this.loadStatsWithFilters();
+        } else {
+          this.msg.warning(message.msg || 
this.i18n.fanyi('common.notify.query-fail'));
+        }
+        this.loading = false;
+      },
+      error: () => {
+        this.loading = false;
+        this.msg.error(this.i18n.fanyi('common.notify.query-fail'));
+      }
+    });
+  }
+
+  loadStatsWithFilters() {
+    const start = this.timeRange?.[0]?.getTime();
+    const end = this.timeRange?.[1]?.getTime();
+    const traceId = this.traceId || undefined;
+    const spanId = this.spanId || undefined;
+    const severity = this.severityNumber || undefined;
+    const severityText = this.severityText || undefined;
+
+    this.logSvc.overviewStats(start, end, traceId, spanId, severity, 
severityText).subscribe({
+      next: message => {
+        if (message.code === 0) {
+          this.overviewStats = message.data || {};
+          this.refreshSeverityChartFromOverview(this.overviewStats);
+        }
+      }
+    });
+
+    this.logSvc.traceCoverageStats(start, end, traceId, spanId, severity, 
severityText).subscribe({
+      next: message => {
+        if (message.code === 0) {
+          this.refreshTraceCoverageChart(message.data || {});
+        }
+      }
+    });
+
+    this.logSvc.trendStats(start, end, traceId, spanId, severity, 
severityText).subscribe({
+      next: message => {
+        if (message.code === 0) {
+          this.refreshTrendChart(message.data?.hourlyStats || {});
+        }
+      }
+    });
+  }
+
+  clearFilters() {
+    this.timeRange = [];
+    this.severityNumber = undefined;
+    this.traceId = '';
+    this.spanId = '';
+    this.severityText = '';
+    this.pageIndex = 1;
+    this.query();
+  }
+
+  toggleStatistics() {
+    this.showStatistics = !this.showStatistics;
+  }
+
+  // Batch selection methods
+  updateCheckedSet(id: string, checked: boolean): void {
+    if (checked) {
+      this.setOfCheckedId.add(id);
+    } else {
+      this.setOfCheckedId.delete(id);
+    }
+  }
+
+  onItemChecked(id: string, checked: boolean): void {
+    this.updateCheckedSet(id, checked);
+    this.refreshCheckedStatus();
+  }
+
+  onAllChecked(checked: boolean): void {
+    this.data.forEach(item => this.updateCheckedSet(this.getLogId(item), 
checked));
+    this.refreshCheckedStatus();
+  }
+
+  refreshCheckedStatus(): void {
+    this.checked = this.data.every(item => 
this.setOfCheckedId.has(this.getLogId(item)));
+    this.indeterminate = this.data.some(item => 
this.setOfCheckedId.has(this.getLogId(item))) && !this.checked;
+  }
+
+  getLogId(item: LogEntry): string {
+    return `${item.timeUnixNano}_${item.traceId || 'no-trace'}`;
+  }
+
+  batchDelete(): void {
+    if (this.setOfCheckedId.size === 0) {
+      this.msg.warning(this.i18n.fanyi('common.notify.no-select-delete'));
+      return;
+    }
+
+    const selectedItems = this.data.filter(item => 
this.setOfCheckedId.has(this.getLogId(item)));
+
+    this.modal.confirm({
+      nzTitle: this.i18n.fanyi('common.confirm.delete-batch'),
+      nzOkText: this.i18n.fanyi('common.button.delete'),
+      nzOkDanger: true,
+      nzCancelText: this.i18n.fanyi('common.button.cancel'),
+      nzOnOk: () => {
+        this.performBatchDelete(selectedItems);
+      }
+    });
+  }
+
+  performBatchDelete(selectedItems: LogEntry[]): void {
+    const timeUnixNanos = selectedItems.filter(item => item.timeUnixNano != 
null).map(item => item.timeUnixNano!);
+
+    if (timeUnixNanos.length === 0) {
+      this.msg.warning(this.i18n.fanyi('common.notify.no-select-delete'));
+      return;
+    }
+
+    this.logSvc.batchDelete(timeUnixNanos).subscribe({
+      next: message => {
+        if (message.code === 0) {
+          this.msg.success(this.i18n.fanyi('common.notify.delete-success'));
+          this.setOfCheckedId.clear();
+          this.refreshCheckedStatus();
+          this.query();
+        } else {
+          this.msg.error(message.msg || 
this.i18n.fanyi('common.notify.delete-fail'));
+        }
+      },
+      error: () => {
+        this.msg.error(this.i18n.fanyi('common.notify.delete-fail'));
+      }
+    });
+  }
+
+  onTablePageChange(params: { pageIndex: number; pageSize: number; sort: any; 
filter: any }) {
+    this.pageIndex = params.pageIndex;
+    this.pageSize = params.pageSize;
+    this.query();
+  }
+
+  getSeverityColor(severityNumber?: number): string {
+    if (!severityNumber) return 'default';
+    if (severityNumber >= 21 && severityNumber <= 24) return 'purple'; // FATAL
+    if (severityNumber >= 17 && severityNumber <= 20) return 'red'; // ERROR
+    if (severityNumber >= 13 && severityNumber <= 16) return 'orange'; // WARN
+    if (severityNumber >= 9 && severityNumber <= 12) return 'blue'; // INFO
+    if (severityNumber >= 5 && severityNumber <= 8) return 'green'; // DEBUG
+    if (severityNumber >= 1 && severityNumber <= 4) return 'default'; // TRACE
+    return 'default';
+  }
+
+  getBodyText(body: any): string {
+    if (!body) return '';
+    if (typeof body === 'string') return body.length > 100 ? `${body.substr(0, 
100)}...` : body;
+    if (typeof body === 'object') {
+      const str = JSON.stringify(body);
+      return str.length > 100 ? `${str.substr(0, 100)}...` : str;

Review Comment:
   Use `substring()` instead of deprecated `substr()` method. Replace 
`body.substr(0, 100)` with `body.substring(0, 100)`.
   ```suggestion
       if (typeof body === 'string') return body.length > 100 ? 
`${body.substring(0, 100)}...` : body;
       if (typeof body === 'object') {
         const str = JSON.stringify(body);
         return str.length > 100 ? `${str.substring(0, 100)}...` : str;
   ```



##########
web-app/src/app/routes/alert/alert-setting/alert-setting.component.ts:
##########
@@ -734,33 +864,39 @@ export class AlertSettingComponent implements OnInit {
       const existsMatch = expr.match(/^(!)?exists\(([^)]+)\)$/);
       if (existsMatch) {
         const [_, not, field] = existsMatch;
+        const parsedRule = this.parseLogFieldAndAttribute(field);
         return {
-          field,
-          operator: not ? '!exists' : 'exists'
-        };
+          field: parsedRule.field,
+          operator: not ? '!exists' : 'exists',
+          ...(parsedRule.objectAttribute && { objectAttribute: 
parsedRule.objectAttribute })
+        } as any;
       }
 
       // Parse string functions (equals, contains, matches)
       const funcMatch = 
expr.match(/^(!)?(?:equals|contains|matches)\(([^,]+),\s*"([^"]+)"\)$/);
       if (funcMatch) {
         const [_, not, field, value] = funcMatch;
         const func = expr.match(/equals|contains|matches/)?.[0] || '';
+        const parsedRule = this.parseLogFieldAndAttribute(field);
         return {
-          field,
+          field: parsedRule.field,
           operator: not ? `!${func}` : func,
-          value
-        };
+          value,
+          ...(parsedRule.objectAttribute && { objectAttribute: 
parsedRule.objectAttribute })
+        } as any;
       }
 
       // Parse numeric comparisons
-      const compareMatch = 
expr.match(/^(\w+(?:\.\w+)*)\s*([><=!]+)\s*(-?\d+(?:\.\d+)?)$/);
+      const compareMatch = 
expr.match(/^([\w.]+)\s*([><=!]+)\s*(-?\d+(?:\.\d+)?)$/);

Review Comment:
   [nitpick] The regular expression is complex and could be difficult to 
maintain. Consider extracting it to a named constant with comments explaining 
the pattern matching logic.
   ```suggestion
         const compareMatch = expr.match(NUMERIC_COMPARISON_REGEX);
   ```



##########
web-app/src/app/routes/log/log-manage/log-manage.component.ts:
##########
@@ -0,0 +1,566 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CommonModule } from '@angular/common';
+import { Component, Inject, OnInit } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { I18NService } from '@core';
+import { ALAIN_I18N_TOKEN } from '@delon/theme';
+import { SharedModule } from '@shared';
+import { EChartsOption } from 'echarts';
+import { NzButtonModule } from 'ng-zorro-antd/button';
+import { NzCardModule } from 'ng-zorro-antd/card';
+import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
+import { NzCollapseModule } from 'ng-zorro-antd/collapse';
+import { NzDatePickerModule } from 'ng-zorro-antd/date-picker';
+import { NzDividerModule } from 'ng-zorro-antd/divider';
+import { NzEmptyModule } from 'ng-zorro-antd/empty';
+import { NzIconModule } from 'ng-zorro-antd/icon';
+import { NzInputModule } from 'ng-zorro-antd/input';
+import { NzListModule } from 'ng-zorro-antd/list';
+import { NzMessageService } from 'ng-zorro-antd/message';
+import { NzModalService, NzModalModule } from 'ng-zorro-antd/modal';
+import { NzPopoverModule } from 'ng-zorro-antd/popover';
+import { NzSpaceModule } from 'ng-zorro-antd/space';
+import { NzStatisticModule } from 'ng-zorro-antd/statistic';
+import { NzTableModule } from 'ng-zorro-antd/table';
+import { NzTagModule } from 'ng-zorro-antd/tag';
+import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
+import { NgxEchartsModule } from 'ngx-echarts';
+
+import { LogEntry } from '../../../pojo/LogEntry';
+import { LogService } from '../../../service/log.service';
+
+@Component({
+  selector: 'app-log-manage',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    SharedModule,
+    NzCardModule,
+    NzTableModule,
+    NzDatePickerModule,
+    NzInputModule,
+    NzButtonModule,
+    NzTagModule,
+    NzToolTipModule,
+    NzEmptyModule,
+    NgxEchartsModule,
+    NzStatisticModule,
+    NzSpaceModule,
+    NzIconModule,
+    NzDividerModule,
+    NzCollapseModule,
+    NzModalModule,
+    NzCheckboxModule,
+    NzPopoverModule,
+    NzListModule
+  ],
+  templateUrl: './log-manage.component.html',
+  styleUrl: './log-manage.component.less'
+})
+export class LogManageComponent implements OnInit {
+  constructor(
+    private logSvc: LogService,
+    private msg: NzMessageService,
+    private modal: NzModalService,
+    @Inject(ALAIN_I18N_TOKEN) private i18n: I18NService
+  ) {}
+
+  // filters
+  timeRange: Date[] = [];
+  severityNumber?: number;
+  severityText?: string;
+  traceId: string = '';
+  spanId: string = '';
+
+  // table with pagination
+  loading = false;
+  data: LogEntry[] = [];
+  pageIndex = 1;
+  pageSize = 20;
+  totalElements = 0;
+  totalPages = 0;
+
+  // charts
+  severityOption!: EChartsOption;
+  trendOption!: EChartsOption;
+  traceCoverageOption!: EChartsOption;
+  severityInstance: any;
+  trendInstance: any;
+  traceCoverageInstance: any;
+
+  // Modal state
+  isModalVisible: boolean = false;
+  selectedLogEntry: LogEntry | null = null;
+
+  // Statistics visibility control
+  showStatistics: boolean = false;
+
+  // Batch selection for table
+  checked = false;
+  indeterminate = false;
+  setOfCheckedId = new Set<string>();
+
+  // overview stats
+  overviewStats: any = {
+    totalCount: 0,
+    fatalCount: 0,
+    errorCount: 0,
+    warnCount: 0,
+    infoCount: 0,
+    debugCount: 0,
+    traceCount: 0
+  };
+
+  // column visibility
+  columnVisibility = {
+    time: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.time') },
+    observedTime: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.observed-time') },
+    severity: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.severity') },
+    body: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.body') },
+    attributes: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.attributes') },
+    resource: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.resource') },
+    traceId: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.trace-id') },
+    spanId: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.span-id') },
+    traceFlags: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.trace-flags') },
+    instrumentation: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.instrumentation') },
+    droppedCount: { visible: true, label: 
this.i18n.fanyi('log.manage.table.column.dropped-count') }
+  };
+
+  // column control visible
+  columnControlVisible = false;
+
+  ngOnInit(): void {
+    this.initChartThemes();
+    this.query();
+  }
+
+  initChartThemes() {
+    this.severityOption = {
+      tooltip: { trigger: 'axis' },
+      xAxis: { type: 'category', data: ['TRACE', 'DEBUG', 'INFO', 'WARN', 
'ERROR', 'FATAL'] },
+      yAxis: { type: 'value' },
+      series: [
+        {
+          type: 'bar',
+          data: [0, 0, 0, 0, 0, 0],
+          itemStyle: {
+            color: function (params: any) {
+              const colors = ['#d9d9d9', '#52c41a', '#1890ff', '#faad14', 
'#ff4d4f', '#722ed1'];
+              return colors[params.dataIndex];
+            }
+          }
+        }
+      ]
+    };
+
+    this.trendOption = {
+      tooltip: { trigger: 'axis' },
+      xAxis: { type: 'category', data: [] },
+      yAxis: { type: 'value' },
+      series: [
+        {
+          type: 'line',
+          data: [],
+          smooth: true,
+          areaStyle: { opacity: 0.3 }
+        }
+      ]
+    };
+
+    this.traceCoverageOption = {
+      tooltip: { trigger: 'item', formatter: '{b}: {c}' },
+      series: [
+        {
+          type: 'pie',
+          radius: ['40%', '70%'],
+          data: [
+            { name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.with-trace'), value: 0, 
itemStyle: { color: '#52c41a' } },
+            { name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.without-trace'), value: 0, 
itemStyle: { color: '#ff4d4f' } },
+            { name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.with-span'), value: 0, 
itemStyle: { color: '#1890ff' } },
+            { name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.complete-trace-info'), value: 
0, itemStyle: { color: '#722ed1' } }
+          ],
+          emphasis: {
+            itemStyle: {
+              shadowBlur: 10,
+              shadowOffsetX: 0,
+              shadowColor: 'rgba(0, 0, 0, 0.5)'
+            }
+          }
+        }
+      ]
+    };
+  }
+
+  onSeverityChartInit(ec: any) {
+    this.severityInstance = ec;
+  }
+  onTrendChartInit(ec: any) {
+    this.trendInstance = ec;
+  }
+  onTraceCoverageChartInit(ec: any) {
+    this.traceCoverageInstance = ec;
+  }
+
+  refreshSeverityChartFromOverview(overviewStats: any) {
+    const traceCount = overviewStats.traceCount || 0;
+    const debugCount = overviewStats.debugCount || 0;
+    const infoCount = overviewStats.infoCount || 0;
+    const warnCount = overviewStats.warnCount || 0;
+    const errorCount = overviewStats.errorCount || 0;
+    const fatalCount = overviewStats.fatalCount || 0;
+
+    const data = [traceCount, debugCount, infoCount, warnCount, errorCount, 
fatalCount];
+    const option: EChartsOption = {
+      ...this.severityOption,
+      series: [
+        {
+          ...(this.severityOption.series as any)?.[0],
+          data
+        }
+      ]
+    };
+    this.severityOption = option;
+    if (this.severityInstance) this.severityInstance.setOption(option);
+  }
+
+  refreshTrendChart(hourlyStats: Record<string, number>) {
+    const sortedHours = Object.keys(hourlyStats).sort();
+    const data = sortedHours.map(hour => hourlyStats[hour]);
+    const option: EChartsOption = {
+      ...this.trendOption,
+      xAxis: {
+        type: 'category',
+        data: sortedHours
+      },
+      series: [{ ...(this.trendOption.series as any)?.[0], data }]
+    };
+    this.trendOption = option;
+    if (this.trendInstance) this.trendInstance.setOption(option);
+  }
+
+  refreshTraceCoverageChart(traceCoverageData: any) {
+    const coverage = traceCoverageData.traceCoverage || {};
+    const data = [
+      {
+        name: this.i18n.fanyi('log.manage.chart.trace-coverage.with-trace'),
+        value: coverage.withTrace || 0,
+        itemStyle: { color: '#52c41a' }
+      },
+      {
+        name: this.i18n.fanyi('log.manage.chart.trace-coverage.without-trace'),
+        value: coverage.withoutTrace || 0,
+        itemStyle: { color: '#ff4d4f' }
+      },
+      {
+        name: this.i18n.fanyi('log.manage.chart.trace-coverage.with-span'),
+        value: coverage.withSpan || 0,
+        itemStyle: { color: '#1890ff' }
+      },
+      {
+        name: 
this.i18n.fanyi('log.manage.chart.trace-coverage.complete-trace-info'),
+        value: coverage.withBothTraceAndSpan || 0,
+        itemStyle: { color: '#722ed1' }
+      }
+    ];
+    const option: EChartsOption = {
+      ...this.traceCoverageOption,
+      series: [{ ...(this.traceCoverageOption.series as any)?.[0], data }]
+    };
+    this.traceCoverageOption = option;
+    if (this.traceCoverageInstance) 
this.traceCoverageInstance.setOption(option);
+  }
+
+  query() {
+    this.loading = true;
+    const start = this.timeRange?.[0]?.getTime();
+    const end = this.timeRange?.[1]?.getTime();
+
+    const obs = this.logSvc.list(
+      start,
+      end,
+      this.traceId,
+      this.spanId,
+      this.severityNumber,
+      this.severityText,
+      this.pageIndex - 1,
+      this.pageSize
+    );
+
+    obs.subscribe({
+      next: message => {
+        if (message.code === 0) {
+          const pageData = message.data;
+          this.data = pageData.content;
+          this.totalElements = pageData.totalElements;
+          this.totalPages = pageData.totalPages;
+          this.pageIndex = pageData.number + 1;
+
+          // Clear selection when data changes
+          this.setOfCheckedId.clear();
+          this.refreshCheckedStatus();
+
+          this.loadStatsWithFilters();
+        } else {
+          this.msg.warning(message.msg || 
this.i18n.fanyi('common.notify.query-fail'));
+        }
+        this.loading = false;
+      },
+      error: () => {
+        this.loading = false;
+        this.msg.error(this.i18n.fanyi('common.notify.query-fail'));
+      }
+    });
+  }
+
+  loadStatsWithFilters() {
+    const start = this.timeRange?.[0]?.getTime();
+    const end = this.timeRange?.[1]?.getTime();
+    const traceId = this.traceId || undefined;
+    const spanId = this.spanId || undefined;
+    const severity = this.severityNumber || undefined;
+    const severityText = this.severityText || undefined;
+
+    this.logSvc.overviewStats(start, end, traceId, spanId, severity, 
severityText).subscribe({
+      next: message => {
+        if (message.code === 0) {
+          this.overviewStats = message.data || {};
+          this.refreshSeverityChartFromOverview(this.overviewStats);
+        }
+      }
+    });
+
+    this.logSvc.traceCoverageStats(start, end, traceId, spanId, severity, 
severityText).subscribe({
+      next: message => {
+        if (message.code === 0) {
+          this.refreshTraceCoverageChart(message.data || {});
+        }
+      }
+    });
+
+    this.logSvc.trendStats(start, end, traceId, spanId, severity, 
severityText).subscribe({
+      next: message => {
+        if (message.code === 0) {
+          this.refreshTrendChart(message.data?.hourlyStats || {});
+        }
+      }
+    });
+  }
+
+  clearFilters() {
+    this.timeRange = [];
+    this.severityNumber = undefined;
+    this.traceId = '';
+    this.spanId = '';
+    this.severityText = '';
+    this.pageIndex = 1;
+    this.query();
+  }
+
+  toggleStatistics() {
+    this.showStatistics = !this.showStatistics;
+  }
+
+  // Batch selection methods
+  updateCheckedSet(id: string, checked: boolean): void {
+    if (checked) {
+      this.setOfCheckedId.add(id);
+    } else {
+      this.setOfCheckedId.delete(id);
+    }
+  }
+
+  onItemChecked(id: string, checked: boolean): void {
+    this.updateCheckedSet(id, checked);
+    this.refreshCheckedStatus();
+  }
+
+  onAllChecked(checked: boolean): void {
+    this.data.forEach(item => this.updateCheckedSet(this.getLogId(item), 
checked));
+    this.refreshCheckedStatus();
+  }
+
+  refreshCheckedStatus(): void {
+    this.checked = this.data.every(item => 
this.setOfCheckedId.has(this.getLogId(item)));
+    this.indeterminate = this.data.some(item => 
this.setOfCheckedId.has(this.getLogId(item))) && !this.checked;
+  }
+
+  getLogId(item: LogEntry): string {
+    return `${item.timeUnixNano}_${item.traceId || 'no-trace'}`;
+  }
+
+  batchDelete(): void {
+    if (this.setOfCheckedId.size === 0) {
+      this.msg.warning(this.i18n.fanyi('common.notify.no-select-delete'));
+      return;
+    }
+
+    const selectedItems = this.data.filter(item => 
this.setOfCheckedId.has(this.getLogId(item)));
+
+    this.modal.confirm({
+      nzTitle: this.i18n.fanyi('common.confirm.delete-batch'),
+      nzOkText: this.i18n.fanyi('common.button.delete'),
+      nzOkDanger: true,
+      nzCancelText: this.i18n.fanyi('common.button.cancel'),
+      nzOnOk: () => {
+        this.performBatchDelete(selectedItems);
+      }
+    });
+  }
+
+  performBatchDelete(selectedItems: LogEntry[]): void {
+    const timeUnixNanos = selectedItems.filter(item => item.timeUnixNano != 
null).map(item => item.timeUnixNano!);
+
+    if (timeUnixNanos.length === 0) {
+      this.msg.warning(this.i18n.fanyi('common.notify.no-select-delete'));
+      return;
+    }
+
+    this.logSvc.batchDelete(timeUnixNanos).subscribe({
+      next: message => {
+        if (message.code === 0) {
+          this.msg.success(this.i18n.fanyi('common.notify.delete-success'));
+          this.setOfCheckedId.clear();
+          this.refreshCheckedStatus();
+          this.query();
+        } else {
+          this.msg.error(message.msg || 
this.i18n.fanyi('common.notify.delete-fail'));
+        }
+      },
+      error: () => {
+        this.msg.error(this.i18n.fanyi('common.notify.delete-fail'));
+      }
+    });
+  }
+
+  onTablePageChange(params: { pageIndex: number; pageSize: number; sort: any; 
filter: any }) {
+    this.pageIndex = params.pageIndex;
+    this.pageSize = params.pageSize;
+    this.query();
+  }
+
+  getSeverityColor(severityNumber?: number): string {
+    if (!severityNumber) return 'default';
+    if (severityNumber >= 21 && severityNumber <= 24) return 'purple'; // FATAL
+    if (severityNumber >= 17 && severityNumber <= 20) return 'red'; // ERROR
+    if (severityNumber >= 13 && severityNumber <= 16) return 'orange'; // WARN
+    if (severityNumber >= 9 && severityNumber <= 12) return 'blue'; // INFO
+    if (severityNumber >= 5 && severityNumber <= 8) return 'green'; // DEBUG
+    if (severityNumber >= 1 && severityNumber <= 4) return 'default'; // TRACE
+    return 'default';
+  }
+
+  getBodyText(body: any): string {
+    if (!body) return '';
+    if (typeof body === 'string') return body.length > 100 ? `${body.substr(0, 
100)}...` : body;
+    if (typeof body === 'object') {
+      const str = JSON.stringify(body);
+      return str.length > 100 ? `${str.substr(0, 100)}...` : str;

Review Comment:
   Use `substring()` instead of deprecated `substr()` method. Replace 
`str.substr(0, 100)` with `str.substring(0, 100)`.
   ```suggestion
       if (typeof body === 'string') return body.length > 100 ? 
`${body.substring(0, 100)}...` : body;
       if (typeof body === 'object') {
         const str = JSON.stringify(body);
         return str.length > 100 ? `${str.substring(0, 100)}...` : str;
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscr...@hertzbeat.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@hertzbeat.apache.org
For additional commands, e-mail: notifications-h...@hertzbeat.apache.org

Reply via email to