This is an automated email from the ASF dual-hosted git repository.
tomsun28 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new c8f2da472e fix: add SSE authentication and correct GreptimeDB SQL
(#4243)
c8f2da472e is described below
commit c8f2da472e02de4a687ce4fde8a665ffca7be965
Author: Yang Chen <[email protected]>
AuthorDate: Sat Jul 25 13:09:13 2026 +0800
fix: add SSE authentication and correct GreptimeDB SQL (#4243)
---
.gitignore | 4 +
.../src/test/resources/sureness.yml | 2 +-
.../resources/greptime/tables/hertzbeat_logs.sql | 2 +-
.../main/resources/greptime/tables/hzb_traces.sql | 2 +-
hertzbeat-manager/src/test/resources/sureness.yml | 2 +-
hertzbeat-startup/src/main/resources/sureness.yml | 2 +-
.../hertzbeat-mysql-iotdb/conf/sureness.yml | 2 +-
.../hertzbeat-mysql-tdengine/conf/sureness.yml | 2 +-
.../conf/sureness.yml | 2 +-
.../conf/sureness.yml | 2 +-
.../conf/sureness.yml | 2 +-
script/sureness.yml | 2 +-
.../log/log-stream/log-stream.component.spec.ts | 29 +++-
.../routes/log/log-stream/log-stream.component.ts | 148 ++++++++++++++-------
14 files changed, 145 insertions(+), 58 deletions(-)
diff --git a/.gitignore b/.gitignore
index 0c6673035e..b3611b7620 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,3 +56,7 @@ application-mysql.yml
application-pg.yml
/package.json
/yarn.lock
+
+# Local Codex workspace files
+.codex/
+AGENTS.md
diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
index cdda55e93a..e7784d87ca 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
+++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
@@ -70,6 +70,7 @@ resourceRole:
- /api/sse/**===post===[admin,user]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin,user]
+ - /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
- /api/otlp/**===post===[admin,user]
- /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.
excludedResource:
- /api/alert/sse/**===*
- - /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
diff --git
a/hertzbeat-log/src/main/resources/greptime/tables/hertzbeat_logs.sql
b/hertzbeat-log/src/main/resources/greptime/tables/hertzbeat_logs.sql
index ea5c4d25f1..a9d7d0dc56 100644
--- a/hertzbeat-log/src/main/resources/greptime/tables/hertzbeat_logs.sql
+++ b/hertzbeat-log/src/main/resources/greptime/tables/hertzbeat_logs.sql
@@ -29,4 +29,4 @@ CREATE TABLE IF NOT EXISTS hertzbeat_logs (
"resource" JSON NULL,
"instrumentation_scope" JSON NULL,
"dropped_attributes_count" INT NULL
-) WITH (append_mode = true);
+) WITH ('append_mode' = 'true');
diff --git a/hertzbeat-log/src/main/resources/greptime/tables/hzb_traces.sql
b/hertzbeat-log/src/main/resources/greptime/tables/hzb_traces.sql
index 64585ce31b..c0d62a0a44 100644
--- a/hertzbeat-log/src/main/resources/greptime/tables/hzb_traces.sql
+++ b/hertzbeat-log/src/main/resources/greptime/tables/hzb_traces.sql
@@ -35,4 +35,4 @@ CREATE TABLE IF NOT EXISTS hzb_traces (
"span_events" JSON NULL,
"span_links" JSON NULL,
PRIMARY KEY("service_name")
-) WITH (append_mode = true, table_data_model = 'greptime_trace_v1');
+) WITH ('append_mode' = 'true', 'table_data_model' = 'greptime_trace_v1');
diff --git a/hertzbeat-manager/src/test/resources/sureness.yml
b/hertzbeat-manager/src/test/resources/sureness.yml
index cdda55e93a..e7784d87ca 100644
--- a/hertzbeat-manager/src/test/resources/sureness.yml
+++ b/hertzbeat-manager/src/test/resources/sureness.yml
@@ -70,6 +70,7 @@ resourceRole:
- /api/sse/**===post===[admin,user]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin,user]
+ - /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
- /api/otlp/**===post===[admin,user]
- /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.
excludedResource:
- /api/alert/sse/**===*
- - /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
diff --git a/hertzbeat-startup/src/main/resources/sureness.yml
b/hertzbeat-startup/src/main/resources/sureness.yml
index 15e2a2b102..0e8a8c975e 100644
--- a/hertzbeat-startup/src/main/resources/sureness.yml
+++ b/hertzbeat-startup/src/main/resources/sureness.yml
@@ -70,6 +70,7 @@ resourceRole:
- /api/mcp/**===post===[admin]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin]
+ - /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
- /api/otlp/**===post===[admin,user]
- /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.
excludedResource:
- /api/alert/sse/**===*
- - /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
index 70609bf26e..ce2da6568e 100644
--- a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
@@ -70,6 +70,7 @@ resourceRole:
- /api/mcp/**===post===[admin]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin]
+ - /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
# 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.
excludedResource:
- /api/alert/sse/**===*
- - /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
diff --git a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
index 70609bf26e..ce2da6568e 100644
--- a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
@@ -70,6 +70,7 @@ resourceRole:
- /api/mcp/**===post===[admin]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin]
+ - /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
# 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.
excludedResource:
- /api/alert/sse/**===*
- - /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
diff --git
a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
index 70609bf26e..ce2da6568e 100644
--- a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
@@ -70,6 +70,7 @@ resourceRole:
- /api/mcp/**===post===[admin]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin]
+ - /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
# 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.
excludedResource:
- /api/alert/sse/**===*
- - /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
diff --git
a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
index edc94a4ef4..9b8736ba75 100644
--- a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
@@ -70,6 +70,7 @@ resourceRole:
- /api/mcp/**===post===[admin]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin]
+ - /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
- /api/otlp/**===post===[admin,user]
- /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.
excludedResource:
- /api/alert/sse/**===*
- - /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
diff --git
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
index 70609bf26e..ce2da6568e 100644
---
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
+++
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
@@ -70,6 +70,7 @@ resourceRole:
- /api/mcp/**===post===[admin]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin]
+ - /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
# 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.
excludedResource:
- /api/alert/sse/**===*
- - /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
diff --git a/script/sureness.yml b/script/sureness.yml
index 47b0270e4c..5c38c9a8e3 100644
--- a/script/sureness.yml
+++ b/script/sureness.yml
@@ -70,6 +70,7 @@ resourceRole:
- /api/mcp/**===post===[admin]
- /api/chat/**===get===[admin,user]
- /api/chat/**===post===[admin]
+ - /api/logs/sse/**===get===[admin,user,guest]
- /api/logs/ingest/**===post===[admin,user]
- /api/otlp/**===post===[admin,user]
- /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.
excludedResource:
- /api/alert/sse/**===*
- - /api/logs/sse/**===*
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
diff --git a/web-app/src/app/routes/log/log-stream/log-stream.component.spec.ts
b/web-app/src/app/routes/log/log-stream/log-stream.component.spec.ts
index 393c1c9310..b3e6908e32 100644
--- a/web-app/src/app/routes/log/log-stream/log-stream.component.spec.ts
+++ b/web-app/src/app/routes/log/log-stream/log-stream.component.spec.ts
@@ -24,18 +24,45 @@ import { LogStreamComponent } from './log-stream.component';
describe('LogStreamComponent', () => {
let component: LogStreamComponent;
let fixture: ComponentFixture<LogStreamComponent>;
+ let fetchSpy: jasmine.Spy<typeof window.fetch>;
beforeEach(async () => {
+ localStorage.setItem('Authorization', 'test-token');
+ fetchSpy = spyOn(window, 'fetch').and.returnValue(new Promise<Response>(()
=> {}));
+
await TestBed.configureTestingModule({
imports: [LogStreamComponent]
- }).compileComponents();
+ })
+ .overrideComponent(LogStreamComponent, {
+ set: { template: '' }
+ })
+ .compileComponents();
fixture = TestBed.createComponent(LogStreamComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
+ afterEach(() => {
+ fixture.destroy();
+ localStorage.removeItem('Authorization');
+ });
+
it('should create', () => {
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)
+ })
+ );
+ });
});
diff --git a/web-app/src/app/routes/log/log-stream/log-stream.component.ts
b/web-app/src/app/routes/log/log-stream/log-stream.component.ts
index e5d7c1093e..c1c0393712 100644
--- a/web-app/src/app/routes/log/log-stream/log-stream.component.ts
+++ b/web-app/src/app/routes/log/log-stream/log-stream.component.ts
@@ -49,6 +49,7 @@ import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { LogEntry } from '../../../pojo/LogEntry';
+import { LocalStorageService } from '../../../service/local-storage.service';
interface ExtendedLogEntry {
original: LogEntry;
@@ -85,7 +86,8 @@ interface ExtendedLogEntry {
export class LogStreamComponent implements OnInit, OnDestroy, AfterViewInit {
@Input() embedded = false;
// SSE connection and state
- private eventSource!: EventSource;
+ private streamAbortController?: AbortController;
+ private reconnectTimer?: ReturnType<typeof setTimeout>;
isConnected: boolean = false;
isConnecting: boolean = false;
@@ -124,7 +126,12 @@ export class LogStreamComponent implements OnInit,
OnDestroy, AfterViewInit {
// ViewChild for log container
@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 {
this.connectToLogStream();
@@ -148,7 +155,7 @@ export class LogStreamComponent implements OnInit,
OnDestroy, AfterViewInit {
}
private connectToLogStream(): void {
- if (this.eventSource) {
+ if (this.streamAbortController) {
this.disconnectFromLogStream();
}
@@ -157,59 +164,108 @@ export class LogStreamComponent implements OnInit,
OnDestroy, AfterViewInit {
// Build filter parameters
const filterParams = this.buildFilterParams();
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;
+
+ this.ngZone.runOutsideAngular(() => {
+ 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');
+ }
- try {
- this.eventSource = new EventSource(url);
+ const decoder = new TextDecoder();
+ let buffer = '';
+ this.ngZone.run(() => {
+ this.isConnected = true;
+ this.isConnecting = false;
+ this.cdr.markForCheck();
+ });
+
+ while (!abortController.signal.aborted) {
+ const { value, done } = await reader.read();
+ if (done) {
+ throw new Error('Log stream connection closed');
+ }
- this.eventSource.onopen = () => {
- this.ngZone.run(() => {
- this.isConnected = true;
- this.isConnecting = false;
- this.cdr.markForCheck();
- });
- };
-
- // Run outside Angular zone to prevent change detection on every message
- this.ngZone.runOutsideAngular(() => {
- this.eventSource.addEventListener('LOG_EVENT', (evt: MessageEvent) => {
- if (!this.isPaused) {
- try {
- const logEntry: LogEntry = JSON.parse(evt.data);
- this.queueLogEntry(logEntry);
- } catch (error) {
- // Silently ignore parse errors in high TPS scenario
- console.error(error);
+ buffer += decoder.decode(value, { stream: true });
+ const frames = buffer.split(/\r?\n\r?\n/);
+ buffer = frames.pop() ?? '';
+
+ 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) {
+ return;
+ }
- this.eventSource.onerror = error => {
- this.ngZone.run(() => {
- this.isConnected = false;
- this.isConnecting = false;
- this.cdr.markForCheck();
+ console.error('Log stream connection error:', error);
+ this.ngZone.run(() => {
+ this.isConnected = false;
+ this.isConnecting = false;
+ this.cdr.markForCheck();
+ });
+
+ this.reconnectTimer = setTimeout(() => {
+ if (!this.isConnected && this.streamAbortController ===
abortController) {
+ this.streamAbortController = undefined;
+ this.connectToLogStream();
+ }
+ }, 5000);
});
-
- // 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;
+ if (this.reconnectTimer) {
+ clearTimeout(this.reconnectTimer);
+ this.reconnectTimer = undefined;
+ }
+ if (this.streamAbortController) {
+ this.streamAbortController.abort();
+ this.streamAbortController = undefined;
}
+ this.isConnected = false;
+ this.isConnecting = false;
}
private buildFilterParams(): string {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]