mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
fix: add SSE authentication and correct GreptimeDB SQL (#4243)
This commit is contained in:
@@ -56,3 +56,7 @@ application-mysql.yml
|
|||||||
application-pg.yml
|
application-pg.yml
|
||||||
/package.json
|
/package.json
|
||||||
/yarn.lock
|
/yarn.lock
|
||||||
|
|
||||||
|
# Local Codex workspace files
|
||||||
|
.codex/
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ resourceRole:
|
|||||||
- /api/sse/**===post===[admin,user]
|
- /api/sse/**===post===[admin,user]
|
||||||
- /api/chat/**===get===[admin,user]
|
- /api/chat/**===get===[admin,user]
|
||||||
- /api/chat/**===post===[admin,user]
|
- /api/chat/**===post===[admin,user]
|
||||||
|
- /api/logs/sse/**===get===[admin,user,guest]
|
||||||
- /api/logs/ingest/**===post===[admin,user]
|
- /api/logs/ingest/**===post===[admin,user]
|
||||||
- /api/otlp/**===post===[admin,user]
|
- /api/otlp/**===post===[admin,user]
|
||||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||||
@@ -81,7 +82,6 @@ resourceRole:
|
|||||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||||
excludedResource:
|
excludedResource:
|
||||||
- /api/alert/sse/**===*
|
- /api/alert/sse/**===*
|
||||||
- /api/logs/sse/**===*
|
|
||||||
- /api/account/auth/**===*
|
- /api/account/auth/**===*
|
||||||
- /api/i18n/**===get
|
- /api/i18n/**===get
|
||||||
- /api/apps/hierarchy===get
|
- /api/apps/hierarchy===get
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ CREATE TABLE IF NOT EXISTS hertzbeat_logs (
|
|||||||
"resource" JSON NULL,
|
"resource" JSON NULL,
|
||||||
"instrumentation_scope" JSON NULL,
|
"instrumentation_scope" JSON NULL,
|
||||||
"dropped_attributes_count" INT NULL
|
"dropped_attributes_count" INT NULL
|
||||||
) WITH (append_mode = true);
|
) WITH ('append_mode' = 'true');
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ CREATE TABLE IF NOT EXISTS hzb_traces (
|
|||||||
"span_events" JSON NULL,
|
"span_events" JSON NULL,
|
||||||
"span_links" JSON NULL,
|
"span_links" JSON NULL,
|
||||||
PRIMARY KEY("service_name")
|
PRIMARY KEY("service_name")
|
||||||
) WITH (append_mode = true, table_data_model = 'greptime_trace_v1');
|
) WITH ('append_mode' = 'true', 'table_data_model' = 'greptime_trace_v1');
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ resourceRole:
|
|||||||
- /api/sse/**===post===[admin,user]
|
- /api/sse/**===post===[admin,user]
|
||||||
- /api/chat/**===get===[admin,user]
|
- /api/chat/**===get===[admin,user]
|
||||||
- /api/chat/**===post===[admin,user]
|
- /api/chat/**===post===[admin,user]
|
||||||
|
- /api/logs/sse/**===get===[admin,user,guest]
|
||||||
- /api/logs/ingest/**===post===[admin,user]
|
- /api/logs/ingest/**===post===[admin,user]
|
||||||
- /api/otlp/**===post===[admin,user]
|
- /api/otlp/**===post===[admin,user]
|
||||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||||
@@ -81,7 +82,6 @@ resourceRole:
|
|||||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||||
excludedResource:
|
excludedResource:
|
||||||
- /api/alert/sse/**===*
|
- /api/alert/sse/**===*
|
||||||
- /api/logs/sse/**===*
|
|
||||||
- /api/account/auth/**===*
|
- /api/account/auth/**===*
|
||||||
- /api/i18n/**===get
|
- /api/i18n/**===get
|
||||||
- /api/apps/hierarchy===get
|
- /api/apps/hierarchy===get
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ resourceRole:
|
|||||||
- /api/mcp/**===post===[admin]
|
- /api/mcp/**===post===[admin]
|
||||||
- /api/chat/**===get===[admin,user]
|
- /api/chat/**===get===[admin,user]
|
||||||
- /api/chat/**===post===[admin]
|
- /api/chat/**===post===[admin]
|
||||||
|
- /api/logs/sse/**===get===[admin,user,guest]
|
||||||
- /api/logs/ingest/**===post===[admin,user]
|
- /api/logs/ingest/**===post===[admin,user]
|
||||||
- /api/otlp/**===post===[admin,user]
|
- /api/otlp/**===post===[admin,user]
|
||||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||||
@@ -84,7 +85,6 @@ resourceRole:
|
|||||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||||
excludedResource:
|
excludedResource:
|
||||||
- /api/alert/sse/**===*
|
- /api/alert/sse/**===*
|
||||||
- /api/logs/sse/**===*
|
|
||||||
- /api/account/auth/**===*
|
- /api/account/auth/**===*
|
||||||
- /api/i18n/**===get
|
- /api/i18n/**===get
|
||||||
- /api/apps/hierarchy===get
|
- /api/apps/hierarchy===get
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ resourceRole:
|
|||||||
- /api/mcp/**===post===[admin]
|
- /api/mcp/**===post===[admin]
|
||||||
- /api/chat/**===get===[admin,user]
|
- /api/chat/**===get===[admin,user]
|
||||||
- /api/chat/**===post===[admin]
|
- /api/chat/**===post===[admin]
|
||||||
|
- /api/logs/sse/**===get===[admin,user,guest]
|
||||||
- /api/logs/ingest/**===post===[admin,user]
|
- /api/logs/ingest/**===post===[admin,user]
|
||||||
|
|
||||||
# config the resource restful api that need bypass auth protection
|
# config the resource restful api that need bypass auth protection
|
||||||
@@ -77,7 +78,6 @@ resourceRole:
|
|||||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||||
excludedResource:
|
excludedResource:
|
||||||
- /api/alert/sse/**===*
|
- /api/alert/sse/**===*
|
||||||
- /api/logs/sse/**===*
|
|
||||||
- /api/account/auth/**===*
|
- /api/account/auth/**===*
|
||||||
- /api/i18n/**===get
|
- /api/i18n/**===get
|
||||||
- /api/apps/hierarchy===get
|
- /api/apps/hierarchy===get
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ resourceRole:
|
|||||||
- /api/mcp/**===post===[admin]
|
- /api/mcp/**===post===[admin]
|
||||||
- /api/chat/**===get===[admin,user]
|
- /api/chat/**===get===[admin,user]
|
||||||
- /api/chat/**===post===[admin]
|
- /api/chat/**===post===[admin]
|
||||||
|
- /api/logs/sse/**===get===[admin,user,guest]
|
||||||
- /api/logs/ingest/**===post===[admin,user]
|
- /api/logs/ingest/**===post===[admin,user]
|
||||||
|
|
||||||
# config the resource restful api that need bypass auth protection
|
# config the resource restful api that need bypass auth protection
|
||||||
@@ -77,7 +78,6 @@ resourceRole:
|
|||||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||||
excludedResource:
|
excludedResource:
|
||||||
- /api/alert/sse/**===*
|
- /api/alert/sse/**===*
|
||||||
- /api/logs/sse/**===*
|
|
||||||
- /api/account/auth/**===*
|
- /api/account/auth/**===*
|
||||||
- /api/i18n/**===get
|
- /api/i18n/**===get
|
||||||
- /api/apps/hierarchy===get
|
- /api/apps/hierarchy===get
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ resourceRole:
|
|||||||
- /api/mcp/**===post===[admin]
|
- /api/mcp/**===post===[admin]
|
||||||
- /api/chat/**===get===[admin,user]
|
- /api/chat/**===get===[admin,user]
|
||||||
- /api/chat/**===post===[admin]
|
- /api/chat/**===post===[admin]
|
||||||
|
- /api/logs/sse/**===get===[admin,user,guest]
|
||||||
- /api/logs/ingest/**===post===[admin,user]
|
- /api/logs/ingest/**===post===[admin,user]
|
||||||
|
|
||||||
# config the resource restful api that need bypass auth protection
|
# config the resource restful api that need bypass auth protection
|
||||||
@@ -77,7 +78,6 @@ resourceRole:
|
|||||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||||
excludedResource:
|
excludedResource:
|
||||||
- /api/alert/sse/**===*
|
- /api/alert/sse/**===*
|
||||||
- /api/logs/sse/**===*
|
|
||||||
- /api/account/auth/**===*
|
- /api/account/auth/**===*
|
||||||
- /api/i18n/**===get
|
- /api/i18n/**===get
|
||||||
- /api/apps/hierarchy===get
|
- /api/apps/hierarchy===get
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ resourceRole:
|
|||||||
- /api/mcp/**===post===[admin]
|
- /api/mcp/**===post===[admin]
|
||||||
- /api/chat/**===get===[admin,user]
|
- /api/chat/**===get===[admin,user]
|
||||||
- /api/chat/**===post===[admin]
|
- /api/chat/**===post===[admin]
|
||||||
|
- /api/logs/sse/**===get===[admin,user,guest]
|
||||||
- /api/logs/ingest/**===post===[admin,user]
|
- /api/logs/ingest/**===post===[admin,user]
|
||||||
- /api/otlp/**===post===[admin,user]
|
- /api/otlp/**===post===[admin,user]
|
||||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||||
@@ -81,7 +82,6 @@ resourceRole:
|
|||||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||||
excludedResource:
|
excludedResource:
|
||||||
- /api/alert/sse/**===*
|
- /api/alert/sse/**===*
|
||||||
- /api/logs/sse/**===*
|
|
||||||
- /api/account/auth/**===*
|
- /api/account/auth/**===*
|
||||||
- /api/i18n/**===get
|
- /api/i18n/**===get
|
||||||
- /api/apps/hierarchy===get
|
- /api/apps/hierarchy===get
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ resourceRole:
|
|||||||
- /api/mcp/**===post===[admin]
|
- /api/mcp/**===post===[admin]
|
||||||
- /api/chat/**===get===[admin,user]
|
- /api/chat/**===get===[admin,user]
|
||||||
- /api/chat/**===post===[admin]
|
- /api/chat/**===post===[admin]
|
||||||
|
- /api/logs/sse/**===get===[admin,user,guest]
|
||||||
- /api/logs/ingest/**===post===[admin,user]
|
- /api/logs/ingest/**===post===[admin,user]
|
||||||
|
|
||||||
# config the resource restful api that need bypass auth protection
|
# config the resource restful api that need bypass auth protection
|
||||||
@@ -77,7 +78,6 @@ resourceRole:
|
|||||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||||
excludedResource:
|
excludedResource:
|
||||||
- /api/alert/sse/**===*
|
- /api/alert/sse/**===*
|
||||||
- /api/logs/sse/**===*
|
|
||||||
- /api/account/auth/**===*
|
- /api/account/auth/**===*
|
||||||
- /api/i18n/**===get
|
- /api/i18n/**===get
|
||||||
- /api/apps/hierarchy===get
|
- /api/apps/hierarchy===get
|
||||||
|
|||||||
+1
-1
@@ -70,6 +70,7 @@ resourceRole:
|
|||||||
- /api/mcp/**===post===[admin]
|
- /api/mcp/**===post===[admin]
|
||||||
- /api/chat/**===get===[admin,user]
|
- /api/chat/**===get===[admin,user]
|
||||||
- /api/chat/**===post===[admin]
|
- /api/chat/**===post===[admin]
|
||||||
|
- /api/logs/sse/**===get===[admin,user,guest]
|
||||||
- /api/logs/ingest/**===post===[admin,user]
|
- /api/logs/ingest/**===post===[admin,user]
|
||||||
- /api/otlp/**===post===[admin,user]
|
- /api/otlp/**===post===[admin,user]
|
||||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||||
@@ -81,7 +82,6 @@ resourceRole:
|
|||||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||||
excludedResource:
|
excludedResource:
|
||||||
- /api/alert/sse/**===*
|
- /api/alert/sse/**===*
|
||||||
- /api/logs/sse/**===*
|
|
||||||
- /api/account/auth/**===*
|
- /api/account/auth/**===*
|
||||||
- /api/i18n/**===get
|
- /api/i18n/**===get
|
||||||
- /api/apps/hierarchy===get
|
- /api/apps/hierarchy===get
|
||||||
|
|||||||
@@ -24,18 +24,45 @@ import { LogStreamComponent } from './log-stream.component';
|
|||||||
describe('LogStreamComponent', () => {
|
describe('LogStreamComponent', () => {
|
||||||
let component: LogStreamComponent;
|
let component: LogStreamComponent;
|
||||||
let fixture: ComponentFixture<LogStreamComponent>;
|
let fixture: ComponentFixture<LogStreamComponent>;
|
||||||
|
let fetchSpy: jasmine.Spy<typeof window.fetch>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
localStorage.setItem('Authorization', 'test-token');
|
||||||
|
fetchSpy = spyOn(window, 'fetch').and.returnValue(new Promise<Response>(() => {}));
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [LogStreamComponent]
|
imports: [LogStreamComponent]
|
||||||
}).compileComponents();
|
})
|
||||||
|
.overrideComponent(LogStreamComponent, {
|
||||||
|
set: { template: '' }
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(LogStreamComponent);
|
fixture = TestBed.createComponent(LogStreamComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.destroy();
|
||||||
|
localStorage.removeItem('Authorization');
|
||||||
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should authenticate the log stream request', () => {
|
||||||
|
expect(fetchSpy).toHaveBeenCalledWith(
|
||||||
|
'/api/logs/sse/subscribe',
|
||||||
|
jasmine.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
headers: jasmine.objectContaining({
|
||||||
|
Accept: 'text/event-stream',
|
||||||
|
Authorization: 'Bearer test-token'
|
||||||
|
}),
|
||||||
|
signal: jasmine.any(AbortSignal)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { NzTagModule } from 'ng-zorro-antd/tag';
|
|||||||
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||||
|
|
||||||
import { LogEntry } from '../../../pojo/LogEntry';
|
import { LogEntry } from '../../../pojo/LogEntry';
|
||||||
|
import { LocalStorageService } from '../../../service/local-storage.service';
|
||||||
|
|
||||||
interface ExtendedLogEntry {
|
interface ExtendedLogEntry {
|
||||||
original: LogEntry;
|
original: LogEntry;
|
||||||
@@ -85,7 +86,8 @@ interface ExtendedLogEntry {
|
|||||||
export class LogStreamComponent implements OnInit, OnDestroy, AfterViewInit {
|
export class LogStreamComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
@Input() embedded = false;
|
@Input() embedded = false;
|
||||||
// SSE connection and state
|
// SSE connection and state
|
||||||
private eventSource!: EventSource;
|
private streamAbortController?: AbortController;
|
||||||
|
private reconnectTimer?: ReturnType<typeof setTimeout>;
|
||||||
isConnected: boolean = false;
|
isConnected: boolean = false;
|
||||||
isConnecting: boolean = false;
|
isConnecting: boolean = false;
|
||||||
|
|
||||||
@@ -124,7 +126,12 @@ export class LogStreamComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
// ViewChild for log container
|
// ViewChild for log container
|
||||||
@ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport;
|
@ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport;
|
||||||
|
|
||||||
constructor(@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService, private cdr: ChangeDetectorRef, private ngZone: NgZone) {}
|
constructor(
|
||||||
|
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
|
||||||
|
private cdr: ChangeDetectorRef,
|
||||||
|
private ngZone: NgZone,
|
||||||
|
private localStorageService: LocalStorageService
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.connectToLogStream();
|
this.connectToLogStream();
|
||||||
@@ -148,7 +155,7 @@ export class LogStreamComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private connectToLogStream(): void {
|
private connectToLogStream(): void {
|
||||||
if (this.eventSource) {
|
if (this.streamAbortController) {
|
||||||
this.disconnectFromLogStream();
|
this.disconnectFromLogStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,59 +164,108 @@ export class LogStreamComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
// Build filter parameters
|
// Build filter parameters
|
||||||
const filterParams = this.buildFilterParams();
|
const filterParams = this.buildFilterParams();
|
||||||
const url = `/api/logs/sse/subscribe${filterParams ? `?${filterParams}` : ''}`;
|
const url = `/api/logs/sse/subscribe${filterParams ? `?${filterParams}` : ''}`;
|
||||||
|
const token = this.localStorageService.getAuthorizationToken();
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Accept: 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
};
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
const abortController = new AbortController();
|
||||||
|
this.streamAbortController = abortController;
|
||||||
|
|
||||||
try {
|
this.ngZone.runOutsideAngular(() => {
|
||||||
this.eventSource = new EventSource(url);
|
fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
signal: abortController.signal
|
||||||
|
})
|
||||||
|
.then(async response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Log stream request failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('Log stream response has no readable body');
|
||||||
|
}
|
||||||
|
|
||||||
this.eventSource.onopen = () => {
|
const decoder = new TextDecoder();
|
||||||
this.ngZone.run(() => {
|
let buffer = '';
|
||||||
this.isConnected = true;
|
this.ngZone.run(() => {
|
||||||
this.isConnecting = false;
|
this.isConnected = true;
|
||||||
this.cdr.markForCheck();
|
this.isConnecting = false;
|
||||||
});
|
this.cdr.markForCheck();
|
||||||
};
|
});
|
||||||
|
|
||||||
// Run outside Angular zone to prevent change detection on every message
|
while (!abortController.signal.aborted) {
|
||||||
this.ngZone.runOutsideAngular(() => {
|
const { value, done } = await reader.read();
|
||||||
this.eventSource.addEventListener('LOG_EVENT', (evt: MessageEvent) => {
|
if (done) {
|
||||||
if (!this.isPaused) {
|
throw new Error('Log stream connection closed');
|
||||||
try {
|
}
|
||||||
const logEntry: LogEntry = JSON.parse(evt.data);
|
|
||||||
this.queueLogEntry(logEntry);
|
buffer += decoder.decode(value, { stream: true });
|
||||||
} catch (error) {
|
const frames = buffer.split(/\r?\n\r?\n/);
|
||||||
// Silently ignore parse errors in high TPS scenario
|
buffer = frames.pop() ?? '';
|
||||||
console.error(error);
|
|
||||||
|
for (const frame of frames) {
|
||||||
|
let eventType = '';
|
||||||
|
const dataLines: string[] = [];
|
||||||
|
for (const line of frame.split(/\r?\n/)) {
|
||||||
|
if (line.startsWith('event:')) {
|
||||||
|
eventType = line.substring(6);
|
||||||
|
} else if (line.startsWith('data:')) {
|
||||||
|
dataLines.push(line.substring(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType !== 'LOG_EVENT' || dataLines.length === 0 || this.isPaused) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logEntry: LogEntry = JSON.parse(dataLines.join('\n'));
|
||||||
|
this.queueLogEntry(logEntry);
|
||||||
|
} catch (error) {
|
||||||
|
// A malformed event must not terminate a long-lived log stream.
|
||||||
|
console.error('Failed to parse log stream event:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
.catch(error => {
|
||||||
|
if (abortController.signal.aborted) {
|
||||||
this.eventSource.onerror = error => {
|
return;
|
||||||
this.ngZone.run(() => {
|
|
||||||
this.isConnected = false;
|
|
||||||
this.isConnecting = false;
|
|
||||||
this.cdr.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-reconnect after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!this.isConnected) {
|
|
||||||
this.connectToLogStream();
|
|
||||||
}
|
}
|
||||||
}, 5000);
|
|
||||||
};
|
console.error('Log stream connection error:', error);
|
||||||
} catch (error) {
|
this.ngZone.run(() => {
|
||||||
this.isConnecting = false;
|
this.isConnected = false;
|
||||||
console.error('Failed to create EventSource:', error);
|
this.isConnecting = false;
|
||||||
}
|
this.cdr.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
if (!this.isConnected && this.streamAbortController === abortController) {
|
||||||
|
this.streamAbortController = undefined;
|
||||||
|
this.connectToLogStream();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private disconnectFromLogStream(): void {
|
private disconnectFromLogStream(): void {
|
||||||
if (this.eventSource) {
|
if (this.reconnectTimer) {
|
||||||
this.eventSource.close();
|
clearTimeout(this.reconnectTimer);
|
||||||
this.isConnected = false;
|
this.reconnectTimer = undefined;
|
||||||
this.isConnecting = false;
|
|
||||||
}
|
}
|
||||||
|
if (this.streamAbortController) {
|
||||||
|
this.streamAbortController.abort();
|
||||||
|
this.streamAbortController = undefined;
|
||||||
|
}
|
||||||
|
this.isConnected = false;
|
||||||
|
this.isConnecting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildFilterParams(): string {
|
private buildFilterParams(): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user