Compare commits

...
1693 Commits
Author SHA1 Message Date
Logic 40e7485be1 Stabilize migration recovery timing test 2026-08-27 19:00:04 +08:00
Logic b4ca58e57b Advance Greptime observability data paths 2026-08-27 18:59:54 +08:00
Logic e1810a45f8 refactor: consolidate observability modules 2026-08-27 10:30:25 +08:00
Logic a7b26c6b66 build: serve shared preview from production bundle 2026-08-26 23:48:16 +08:00
Logic d6a801e36f test: launch observability e2e through startup boundary 2026-08-26 20:45:09 +08:00
Logic 34e05a05ce test: align observability e2e request contract 2026-08-26 18:10:18 +08:00
Logic 1f73694892 test: restore observability context wiring 2026-08-26 17:51:49 +08:00
Logic 60a682a223 build: remove orphan mysql r2dbc e2e module 2026-08-26 17:37:08 +08:00
Logic 2c5754302c refactor: reconcile merged frontend boundaries 2026-08-26 17:25:22 +08:00
Logic d40787953d feat: isolate observability signal queries 2026-08-26 15:49:06 +08:00
Logic af2e534dfb feat: bound concurrent OTLP writes 2026-08-26 15:28:10 +08:00
Logic fd6f5590bf feat: preserve observability backpressure signals 2026-08-26 15:20:03 +08:00
Logic 906f4283b6 refactor: isolate OTLP guide construction 2026-08-26 15:09:49 +08:00
Logic cc46063bea Preserve typed AI workflow arguments 2026-08-26 14:55:27 +08:00
Logic 3398db3c71 Merge validated React operational workflows 2026-08-26 14:50:47 +08:00
Logic b84cb3a73a build: add immutable aws preview sync 2026-08-26 12:20:38 +08:00
Logic fcdcfe2428 test: align deployment route contracts 2026-08-26 12:20:27 +08:00
Logic 0db1e44f4a test: make collector e2e portable on arm 2026-08-26 12:20:11 +08:00
Logic da3d2a4e64 fix: stabilize managed startup transitions 2026-08-26 12:19:41 +08:00
Logic de1850a6e8 feat: authenticate stored secret payloads 2026-08-26 12:19:06 +08:00
Logic f82c340a16 refactor: preserve shared React runtime refinements 2026-08-26 10:10:38 +08:00
Logic b8ad16b74c test: split operator workflow suites 2026-08-26 10:06:45 +08:00
Logic 720d20bdc4 feat: preserve guided instrumentation workflow 2026-08-26 09:54:46 +08:00
Logic 4378c83039 feat: preserve monitor investigation workflow 2026-08-26 09:50:58 +08:00
Logic e1d8eb0627 fix: restore managed database startup 2026-08-25 20:01:18 +08:00
Logic 4200ffab34 chore: checkpoint current 2.0 development state 2026-08-25 19:01:33 +08:00
Logic c9c310d8f6 feat(ai): add schedule management UI 2026-08-14 08:45:06 +08:00
Yang Chen 284cab8544 feat(ai): add system agent schedules 2026-08-14 08:04:49 +08:00
Yang Chen 2b16b47a0a feat(ai): expose additional MCP tools 2026-08-14 07:58:03 +08:00
Logic 4172c862a6 Complete 2.0.0 setup and AI runtime integration 2026-08-14 00:18:58 +08:00
Logic 1c35254ff7 Merge managed setup runtime into 2.0.0 2026-08-13 23:20:54 +08:00
Logic 9a2617ff0e feat(ai): add operator investigation workspace
Add the React Lite workspace, safe streaming transport, provider administration, cross-page context handoff, and localized route integration.

AI assistance: used for draft implementation and test iteration.

Human validation: Node 22 full 5,343-test suite, build, architecture, lint, typecheck, i18n, format, bundle, dead-code, and Java 25 23-module package passed.

Risk notes: browser visual proof remains blocked by the existing local backend setup-status admission response.
2026-08-13 22:19:29 +08:00
Logic 842775b3df feat(ai): include gateway in startup runtime 2026-08-13 20:55:28 +08:00
Logic c831cfd146 feat(ai): add cross-signal investigation tools 2026-08-13 20:51:31 +08:00
Logic 0fda5be2bd feat(ai): carry investigation context across runs 2026-08-13 20:41:55 +08:00
Logic 7d18c85dc4 feat(ai): harden gateway persistence and authorization 2026-08-13 20:31:02 +08:00
Logic 82f2440fd0 feat(ai): adapt agent gateway to 2.0 runtime
Adapted-From: 659ebedc87
2026-08-13 20:13:34 +08:00
Yang Chen ce05942c77 refactor(ai-gateway): remove legacy sensitive approval params
Original-Commit: 1c26af1435
2026-08-13 19:54:42 +08:00
Yang Chen a7e94a4b38 feat(ai): add model provider management
Original-Commit: fd09cf27f4
2026-08-13 19:54:41 +08:00
Yang Chen d83bbd7486 refactor(ai): remove test-only gateway APIs
Original-Commit: 3eed6edbb7
2026-08-13 19:54:41 +08:00
Yang Chen 0291c46bec fix(ai): scope session queries by envelope
Original-Commit: 27b1081371
2026-08-13 19:54:40 +08:00
Yang Chen 2e425706f8 refactor(ai): remove unused gateway queries
Original-Commit: 811d0080e8
2026-08-13 19:54:40 +08:00
Yang Chen 873ed9dc5d refactor(ai): redesign runtime context compaction
Original-Commit: 501efce7fe
2026-08-13 19:54:39 +08:00
Yang Chen b38ebb96b2 refactor(ai): simplify runtime model response handling
Original-Commit: 46f2b05133
2026-08-13 19:54:39 +08:00
Yang Chen 8833d8a146 refactor(ai): standardize runtime model providers
Original-Commit: e514a44283
2026-08-13 19:54:38 +08:00
Yang Chen c5a202a842 refactor(ai): simplify runtime stream backpressure config
Original-Commit: 78c404adc7
2026-08-13 19:54:37 +08:00
Yang Chen 5c126ea8f7 feat(ai): add agent gateway
Original-Commit: 659ebedc87
2026-08-13 19:54:23 +08:00
Logic ec51227a8c feat: refine monitor workbench and login experience 2026-08-12 19:45:52 +08:00
Logic 00f91d4433 Merge remote-tracking branch 'apache/2.0.0' into codex/react-lite-frontend
# Conflicts:
#	web-app/eslint.config.mjs
#	web-next/components/pages/login-form.submit.test.tsx
#	web-next/components/pages/login-form.test.tsx
#	web-next/components/shell/app-frame.chrome.test.tsx
#	web-next/package-lock.json
#	web-next/package.json
#	web-next/scripts/parity/seed-state-lib.test.ts
#	web-next/scripts/release-shell-smoke.test.ts
#	web-next/scripts/release-shell.mjs
#	web-next/scripts/standalone-server.mjs
2026-08-11 11:02:57 +08:00
Logic d4b1db08f7 Complete setup and monitor editor parity 2026-08-11 11:01:45 +08:00
Logic 1eb35646d7 Align startup release readiness contract 2026-08-11 00:36:03 +08:00
Logic 7d7e396a41 Stabilize startup context transitions 2026-08-11 00:33:50 +08:00
Logic 03946f3c8a Stabilize frontend release validation 2026-08-11 00:11:56 +08:00
Logic 021cbb176f Wire managed metadata migration 2026-08-10 23:23:32 +08:00
Logic 9217f669ad Inspect migration targets safely 2026-08-10 22:12:30 +08:00
Logic 420d280e5d Manage retained migration lifecycle 2026-08-10 21:34:04 +08:00
Logic 54be0dfde0 Coordinate managed migration commands 2026-08-10 20:58:38 +08:00
Logic cc95d4a9ad Record durable migration execution failures 2026-08-10 19:32:39 +08:00
Logic ae4c15d490 Bind migration requests and prepared exports 2026-08-10 19:14:23 +08:00
Logic 5b4f18fdbc Reconcile migration state before Spring startup 2026-08-10 18:33:27 +08:00
Logic 27633afb26 Guard managed migration startup admission 2026-08-10 17:59:22 +08:00
Logic 64a153f8e8 Coordinate managed migration startup recovery 2026-08-10 17:12:51 +08:00
Logic 649c6413f0 Verify active migration startup configuration 2026-08-10 16:52:45 +08:00
Logic 65a8026baa Verify managed migration startup target 2026-08-10 16:38:51 +08:00
Logic ffa2adbc91 Reconcile managed metadata startup 2026-08-10 16:08:09 +08:00
Logic f0e016beb1 Activate retained metadata cutovers 2026-08-10 15:45:59 +08:00
Logic f753f21c22 Handoff retained metadata copy journal 2026-08-10 15:20:42 +08:00
Logic c83565b818 Prepare metadata cutovers durably 2026-08-10 14:54:28 +08:00
Logic 7114c0e71b Model metadata preparation recovery 2026-08-10 14:29:03 +08:00
Logic 5fb86ee9b0 Confirm migration candidate durability 2026-08-10 14:11:33 +08:00
Logic bb3aef3ecf Confirm metadata operation journal writes 2026-08-10 13:53:28 +08:00
Logic 9ba2f5cd17 Prepare metadata cutovers durably 2026-08-10 13:37:25 +08:00
Logic 5046236b82 Retain metadata cutover maintenance 2026-08-10 13:21:39 +08:00
Logic d74564cc88 Stage managed metadata migration targets 2026-08-10 12:40:42 +08:00
Logic 6f8258cc7d Classify metadata migration restart state 2026-08-10 12:22:17 +08:00
Logic 8efbb02603 Bound target JDBC connections 2026-08-10 11:53:17 +08:00
Logic 4322c0d21d Bound target schema provisioning 2026-08-10 09:49:26 +08:00
Logic 7dffb24c4b Compose metadata copy maintenance 2026-08-10 08:52:50 +08:00
Logic f5b4d7401c Bound metadata migration execution 2026-08-10 08:20:27 +08:00
Logic 7afb6501e7 Activate managed migration candidates 2026-08-10 07:25:12 +08:00
Logic c4190b4c0a Persist metadata migration candidates 2026-08-10 06:47:40 +08:00
Logic aa3704bd73 Version metadata migration journal 2026-08-10 05:57:09 +08:00
Logic 6588f08563 Add JDBC metadata copy primitive 2026-08-10 05:37:05 +08:00
Logic 9c40cd466c Coordinate standalone deployment ownership 2026-08-10 03:31:32 +08:00
Logic 6bc71db9f1 Coordinate metadata migration maintenance 2026-08-10 03:07:08 +08:00
Logic dd3039611c Quiesce collector and alert metadata writers 2026-08-10 02:09:34 +08:00
Logic 2d2a423c79 Quiesce metadata control-plane writers 2026-08-10 00:45:40 +08:00
Logic d0b4c194ea Gate metadata writes during maintenance 2026-08-09 23:47:44 +08:00
Logic b0ba220883 Persist metadata migration operations 2026-08-09 22:12:41 +08:00
Logic fe70a592b9 Clarify metadata migration admission 2026-08-09 20:35:42 +08:00
Logic 293b983aa4 Provision current metadata schemas 2026-08-09 20:14:39 +08:00
Logic 100ebf9ab5 Clarify setup completion command result 2026-08-09 19:21:10 +08:00
Logic b7c3628cbd Ratchet frontend bundle budget 2026-08-09 19:19:20 +08:00
Logic a09e0cdc33 Add deployment migration workspace 2026-08-09 19:17:16 +08:00
Logic ad45b32881 Define metadata migration contract 2026-08-09 16:09:57 +08:00
Logic 5294816012 Align setup login redirect 2026-08-09 15:05:44 +08:00
Logic 50b17faa55 Make setup runtime transitions durable 2026-08-09 15:00:17 +08:00
Logic 134ffc61e7 Complete optional setup workflow 2026-08-09 14:53:42 +08:00
Logic b8b1734e23 Stabilize setup workflow lifecycle 2026-08-09 13:51:38 +08:00
Logic ea09f43580 Align startup setup contracts 2026-08-09 13:00:52 +08:00
Logic a6cadbc7b7 Refine setup local file handling 2026-08-09 12:30:46 +08:00
Logic 2702c55e73 Add operator-owned setup public addresses 2026-08-09 08:54:31 +08:00
Logic 7d0b84a20c Require setup runtime dependencies 2026-08-09 07:18:07 +08:00
Logic e6a45c721c Unify setup configuration orchestration 2026-08-09 06:57:54 +08:00
Logic 6dd503e83b Centralize optional setup transitions 2026-08-09 06:42:06 +08:00
Logic 61f5dd325c Clear managed configuration buffers 2026-08-09 06:12:23 +08:00
Logic 865fcc6a9d Harden setup failure diagnostics 2026-08-09 05:38:36 +08:00
Logic a866b0f97b Stream setup configuration exports 2026-08-09 05:12:17 +08:00
Logic 2d6d146fc5 Preserve safe startup failure diagnostics 2026-08-09 04:18:12 +08:00
Logic 6e63b80e72 Harden setup JDBC probe lifecycle 2026-08-09 03:51:04 +08:00
Logic 697c36e20c Preserve safe setup failure diagnostics 2026-08-09 02:48:12 +08:00
Logic 1e0bcc6547 Preserve setup operations across startup 2026-08-09 02:23:39 +08:00
Logic 801ff3ad02 Wire setup options to runtime consumers 2026-08-09 01:19:26 +08:00
Logic 57548d9d08 Keep setup reachable in gated runtime 2026-08-08 23:42:56 +08:00
Logic ca39b5ec08 Add first setup administrator step 2026-08-08 23:17:34 +08:00
Logic 4f6fd62a6c Normalize setup administrator identity 2026-08-08 23:00:15 +08:00
Logic e20f99d9ac Add first-install setup configuration flow 2026-08-08 22:33:46 +08:00
Logic 733bc5e93f Unify setup lifecycle transitions 2026-08-08 21:57:11 +08:00
Logic 70c9034b89 Complete setup workflow and validation 2026-08-08 19:05:03 +08:00
Logic 410161c3f4 Add database-backed setup identity 2026-08-08 12:11:36 +08:00
Logic 5a399175cb Add managed deployment configuration 2026-08-08 07:59:12 +08:00
Logic 29ac811916 Add setup-gated application runtime 2026-08-08 03:26:28 +08:00
Logic 3c79e5c31e Define setup and deployment contracts 2026-08-07 22:58:49 +08:00
Logic db8f745d74 Keep alert ingress address operator-owned 2026-08-07 22:49:13 +08:00
Logic b4ff84110b Align shell and settings form layout 2026-08-07 18:57:02 +08:00
Logic 5eccabbfd4 Reduce decorative page boundaries 2026-08-07 18:35:33 +08:00
Logic c179e270c9 Unify header and form action alignment 2026-08-07 18:18:46 +08:00
Logic 3204a4525e Align header brand and navigation columns 2026-08-07 18:12:41 +08:00
Logic 7cc18f64f3 Omit null entries from alert payload maps 2026-08-07 18:02:17 +08:00
Logic d579755cd9 Handle nullable historical alert labels 2026-08-07 18:02:02 +08:00
Logic 4ae6f14801 Restore semantic runtime status colors 2026-08-07 17:07:54 +08:00
Logic 1788fcb069 Reduce authenticated shell request fanout 2026-08-07 16:46:57 +08:00
Logic 74a699acad Restore passport product context 2026-08-07 15:35:40 +08:00
Logic b2c2335aa0 Refine selected list item styling 2026-08-07 12:04:50 +08:00
Logic 209ef4c2a8 Replace YAML scroll relay with native merge view 2026-08-07 11:52:41 +08:00
Logic b64ae04aa0 Centralize monitor workspace actions 2026-08-07 10:28:11 +08:00
Logic b62f10c14a Stabilize YAML comparison scrolling 2026-08-07 10:09:47 +08:00
Logic 63051798a8 Refine monitor definition workspace actions 2026-08-07 09:51:38 +08:00
Logic 5694772e9f Refine application shell branding and controls 2026-08-07 09:33:08 +08:00
Logic 54d8296a5f Restore frontend maintainability gates 2026-08-07 00:41:00 +08:00
Logic 4ae8fc8599 Restore monitor and topology route behavior 2026-08-06 22:30:25 +08:00
Logic 9824570911 Handle empty alert integration catalogs 2026-08-06 22:00:36 +08:00
Logic 5b5217373a Repair public evidence and route recovery 2026-08-06 21:55:50 +08:00
Logic 7763124a06 Render log history as a trend chart 2026-08-06 21:24:54 +08:00
Logic 1a8a80186e Restore Explore signal evidence behavior 2026-08-06 21:05:46 +08:00
Logic c2099f64fd Recover uncertain sensitive operations 2026-08-06 20:31:59 +08:00
Logic 249687c97c Protect navigation and monitor batch selection 2026-08-06 19:53:28 +08:00
Logic f4deccec03 Allow builtin definition overrides 2026-08-06 19:25:05 +08:00
Logic 779b0c87ce Complete monitor definition lifecycle controls 2026-08-06 19:12:19 +08:00
Logic f83dbe382e Refine shell navigation hierarchy 2026-08-06 18:40:48 +08:00
Logic 8976255d0c Unify themed scrollbar styling 2026-08-06 18:34:58 +08:00
Logic a264e8343a Refine monitor definition comparison 2026-08-06 18:32:03 +08:00
Logic 2d2705d985 Add explicit light and dark theme switch 2026-08-06 17:29:09 +08:00
Logic a07b84523f Fix monitor definition test fixture typing 2026-08-06 17:23:15 +08:00
Logic 3ab089a7c9 Restore direct monitor definition editing 2026-08-06 16:41:59 +08:00
Logic c997a608f0 Restore monitor definition YAML comparison 2026-08-06 15:55:09 +08:00
Logic 8b0b690651 Allow built-in monitor definition overrides 2026-08-06 15:10:01 +08:00
Logic 7bd7603d33 Hide irrelevant embedded collector report warning 2026-08-06 14:42:52 +08:00
Logic 7ce055d2ce Treat embedded collector as server-local 2026-08-06 14:42:43 +08:00
Logic 68c70a9b39 Use plain runtime status wording 2026-08-06 12:19:27 +08:00
Logic e1e47b84aa Keep header status states visible 2026-08-06 11:24:21 +08:00
Logic 2e10ca3aba Remove duplicate dashboard status summary 2026-08-06 10:37:42 +08:00
Logic 90ba362cda Reduce dashboard divider noise 2026-08-06 10:13:59 +08:00
Logic ab04c63b4c Clarify embedded and hybrid collector roles 2026-08-06 09:52:17 +08:00
Logic e12e774a23 feat: explain telemetry destination routes 2026-08-06 00:53:26 +08:00
Logic 08960234e4 feat: verify collector OTLP gateway runtime 2026-08-06 00:27:28 +08:00
Logic 9e2e957282 refactor: separate server and collector OTLP intake 2026-08-06 00:15:13 +08:00
Logic d92d11a5d9 fix: clarify collector OTLP gateway ownership 2026-08-05 23:58:22 +08:00
Logic 4685388063 fix: reduce operational divider noise 2026-08-05 23:04:48 +08:00
Logic e8fcaf4a68 fix: group operational search actions 2026-08-05 22:52:57 +08:00
Logic 607f6fc1d1 fix: bound operational search fields 2026-08-05 21:53:50 +08:00
Logic e10a107d78 fix: balance bulletin editor modal 2026-08-05 21:32:18 +08:00
Logic 3a5160379b fix: center bounded editor workspaces 2026-08-05 20:36:12 +08:00
Logic 85b5a54aeb fix: center operational form workspaces 2026-08-05 20:18:17 +08:00
Logic 8b0564dead fix: restore monitor form alignment 2026-08-05 20:14:19 +08:00
Logic 5ee9a73b31 chore: remove unused frontend exports 2026-08-05 19:37:53 +08:00
Logic a24e0a5aef test: stabilize final frontend contracts 2026-08-05 19:21:52 +08:00
Logic ca72913d00 refactor: isolate instrumentation readiness 2026-08-05 18:58:34 +08:00
Logic faa092476e refactor: split monitor definition workspace 2026-08-05 18:46:03 +08:00
Logic 66b7761278 refactor: separate entity evidence mapping 2026-08-05 18:41:16 +08:00
Logic 3f6117cfd8 refactor: split monitor feature concerns 2026-08-05 18:37:12 +08:00
Logic 84e4491571 refactor: split instrumentation controller concerns 2026-08-05 18:25:31 +08:00
Logic 5f35d4d0f2 refactor: split alert notice page composition 2026-08-05 18:22:48 +08:00
Logic 0c9afd013c feat: align public status evidence 2026-08-05 18:15:02 +08:00
Logic ccee0abcb1 fix: confirm default login and public 404 2026-08-05 17:55:23 +08:00
Logic e5093765f1 feat: make topology empty states actionable 2026-08-05 17:34:41 +08:00
Logic 2a78de58f6 fix: accept entity evidence compatibility shape 2026-08-05 17:05:55 +08:00
Logic 989838b370 feat: connect entity topology inspection 2026-08-05 16:59:14 +08:00
Logic 937acf0d47 fix: recover instrumentation metadata loading 2026-08-05 16:35:57 +08:00
Logic c11ada62e0 feat: connect notification configuration workflow 2026-08-05 16:20:39 +08:00
Logic 3323fe0a7e feat: restore monitor definition workspace 2026-08-05 15:51:26 +08:00
Logic e73ab67194 feat: restore operational dashboard summary 2026-08-05 15:15:41 +08:00
Logic a4ecbb8552 refactor: restore bulletin metric workspace 2026-08-05 09:44:45 +08:00
Logic c3a12b946d refactor: align bulletin editor with angular modal 2026-08-05 01:02:43 +08:00
Logic da776d95db fix: make monitor detail failures recoverable 2026-08-05 00:10:43 +08:00
Logic 3d6aad4b20 refactor: collapse nested operational separators 2026-08-04 23:43:46 +08:00
Logic 1ef38b2297 refactor: remove redundant start page explanations 2026-08-04 23:16:45 +08:00
Logic a25c8d85b8 refactor: use operational start page copy 2026-08-04 22:57:05 +08:00
Logic 80df634bb0 refactor: compact operational query actions 2026-08-04 22:00:37 +08:00
Logic abfb9c804a refactor: place plugins under administration 2026-08-04 21:23:57 +08:00
Logic 05f0b2a4d4 refactor: restore two-level navigation 2026-08-04 20:37:30 +08:00
Logic fb1a1ae117 fix: default signal query to metrics 2026-08-04 19:29:25 +08:00
Logic bf1e90e803 refactor: group contextual navigation 2026-08-04 17:02:44 +08:00
Logic 31eae17414 refactor: simplify global navigation 2026-08-04 16:41:42 +08:00
Logic 893197a760 refactor: share compact form rails 2026-08-04 10:00:28 +08:00
Logic b99c22e5df fix: align monitor editor form 2026-08-04 00:53:35 +08:00
Logic afb80d5f66 fix: restore monitor metric visual parity 2026-08-03 23:51:11 +08:00
Logic ca9e8937b7 refactor: prioritize monitor data inspection 2026-08-03 21:27:14 +08:00
Logic 56d83f071b feat: restore lazy history charts 2026-08-03 18:42:15 +08:00
Logic 429899321f feat: restore grouped realtime metrics 2026-08-03 17:06:16 +08:00
Logic 05d4426614 feat: show pending monitor availability 2026-08-03 15:52:24 +08:00
Logic 8cfbd4c287 perf: defer monitor availability detection 2026-08-03 15:45:36 +08:00
Logic 5b65ebd196 perf: unblock monitor save acknowledgement 2026-08-03 15:24:04 +08:00
Logic 8da89c6764 fix: preserve monitor command feedback 2026-08-03 14:29:52 +08:00
Logic 8edb5e0fa0 fix: restore discovery form order 2026-08-03 12:28:02 +08:00
Logic 27ceea7a2d fix: center monitor editor form 2026-08-03 11:33:22 +08:00
Logic 411b9b2d7a fix: restore single-column monitor editor 2026-08-03 11:17:09 +08:00
Logic 29f6f9b904 fix: unify monitor type selection 2026-08-03 10:29:37 +08:00
Logic b1f906cbf9 feat: restore monitor type picker 2026-08-03 09:48:59 +08:00
Logic 1f661e8bc5 style: reduce dashboard card treatment 2026-08-03 08:48:15 +08:00
Logic 2f861a4663 fix: bind managed collector guide tokens 2026-08-03 03:13:27 +08:00
Logic eb91c779c8 merge: integrate hybrid collector backend 2026-08-03 02:59:54 +08:00
Logic 0227997f7b test: align final frontend contracts 2026-08-03 02:53:27 +08:00
Logic 446faec7f6 test(startup): bind external intake authentication 2026-08-03 00:43:32 +08:00
Logic 9a983d5b4f test(e2e): assert negative query shapes 2026-08-03 00:30:57 +08:00
Logic 90e784e199 test(e2e): tighten collector identity proof 2026-08-03 00:26:39 +08:00
Logic a4c654e14f fix: gate instrumentation by route roles 2026-08-03 00:22:28 +08:00
Logic dc99424924 feat: expose entity evidence sources 2026-08-03 00:03:14 +08:00
Logic f04294279d test(e2e): prove managed Prometheus intake 2026-08-03 00:01:05 +08:00
Logic 4a66fd2f91 fix: reconcile uncertain monitor creates 2026-08-02 23:30:24 +08:00
Logic d3e62bcaef Consolidate instrumentation public routes 2026-08-02 23:29:07 +08:00
Logic 859eb678d4 Extract telemetry evidence source summaries 2026-08-02 23:12:56 +08:00
Logic 48ab70f71e Expose entity evidence provenance 2026-08-02 23:05:41 +08:00
Logic 81d55cc925 fix: clarify collector source lifecycle 2026-08-02 22:57:41 +08:00
Logic 2cc5ef78a4 Centralize telemetry snapshot sources 2026-08-02 22:46:12 +08:00
Logic 91a293b46d Keep OTLP bindings source-backed 2026-08-02 22:39:33 +08:00
Logic 97b05e06d8 fix: tighten dashboard start layout 2026-08-02 22:37:13 +08:00
Logic 93707948ff Trust authenticated OTLP collector identity 2026-08-02 22:21:16 +08:00
Logic 80f390d2bf feat: add dashboard start paths 2026-08-02 22:18:50 +08:00
Logic 184e8f71dd Keep OTLP overview metrics source-backed 2026-08-02 21:59:32 +08:00
Logic 96f16f3dfc Keep operational tables within result regions 2026-08-02 11:50:45 +08:00
Logic ac4e677e01 Localize alert integration guidance 2026-08-02 11:39:20 +08:00
Logic 37f8ababc9 Align dashboard empty state test 2026-08-02 06:58:21 +08:00
Logic d3d338da11 Clarify object storage page states 2026-08-02 06:25:49 +08:00
Logic b2377e90ed Separate instrumentation question styles 2026-08-02 06:17:16 +08:00
Logic a34cc4b7e6 Clarify shared page responsibilities 2026-08-02 06:14:22 +08:00
Logic 4a8239f132 Split alert presentation responsibilities 2026-08-02 06:00:36 +08:00
Logic 0a2a2b0102 Preserve authentication presentation boundary 2026-08-02 05:39:17 +08:00
Logic a4114eadbc Unify alert integration route states 2026-08-02 05:30:17 +08:00
Logic 5efb02a018 Unify application route states 2026-08-02 05:19:17 +08:00
Logic 1f0574403c Unify global control styling 2026-08-02 05:10:30 +08:00
Logic fa0a47d18f Refine authentication surfaces 2026-08-02 05:02:07 +08:00
Logic 5af18d46f0 Refine public status evidence 2026-08-02 04:48:35 +08:00
Logic ac98a7ad72 Unify status management workspace 2026-08-02 04:43:54 +08:00
Logic 06095f60c1 Unify bulletin workspace 2026-08-02 04:33:58 +08:00
Logic d60372e70c Refine object storage states 2026-08-02 04:26:09 +08:00
Logic e533c6126d Refine system settings states 2026-08-02 04:20:34 +08:00
Logic 017edee786 Refine notification channel services 2026-08-02 04:13:38 +08:00
Logic fe308118aa Unify notification receiver workspace 2026-08-02 04:05:53 +08:00
Logic 8426e99d64 Unify notification template workspace 2026-08-02 03:57:37 +08:00
Logic 91f3a7edc1 Unify notification rule workspace 2026-08-02 03:50:27 +08:00
Logic 58299fa16c Unify API token workspace 2026-08-02 03:39:33 +08:00
Logic 177add6939 Unify monitor definition catalog 2026-08-02 03:31:50 +08:00
Logic f68f400832 Unify label management workspace 2026-08-02 03:22:32 +08:00
Logic c92f36e531 Refine collector management workspace 2026-08-02 03:16:43 +08:00
Logic 7fe54e2450 Unify plugin management workspace 2026-08-02 03:08:36 +08:00
Logic 187872a618 Unify alert policy workspaces 2026-08-02 03:03:23 +08:00
Logic e2b8f6f0c5 Unify alert center workspace 2026-08-02 02:41:16 +08:00
Logic 124b387b41 Refine instrumentation guidance workspace 2026-08-02 02:32:05 +08:00
Logic e9b1a3d9a7 Refine instrumentation source directory 2026-08-02 02:15:28 +08:00
Logic 428e38d55a Unify Explore signal workspace 2026-08-02 02:01:43 +08:00
Logic 3124511a43 Refine topology graph workspace 2026-08-02 01:46:25 +08:00
Logic a0594369c9 Refine monitor detail workspace 2026-08-02 01:25:16 +08:00
Logic 7130b11e31 Repair entity style typing 2026-08-02 01:13:57 +08:00
Logic 6922f4418b Refine monitor authoring hierarchy 2026-08-02 01:01:48 +08:00
Logic 4851418373 Refine resource editor layout 2026-08-02 00:47:17 +08:00
Logic 943016ff64 Refine resource detail workspace 2026-08-02 00:41:10 +08:00
Logic 82d09efbbe Refine resource catalog hierarchy 2026-08-02 00:28:22 +08:00
Logic 29d8e2a63b Unify operational page hierarchy 2026-08-02 00:14:59 +08:00
Logic ca59de20b6 Keep entity operations summary internal 2026-08-01 23:09:12 +08:00
Logic d26a6f62ca Clarify entity discovery control validation 2026-08-01 22:44:28 +08:00
Logic 2ea6043e99 Handle alert integration navigation promise 2026-08-01 22:39:05 +08:00
Logic 8cd68cb442 Clarify object store draft state 2026-08-01 22:33:12 +08:00
Logic 04bf0a0a0d Refine topology evidence presentation 2026-08-01 22:29:15 +08:00
Logic 2e5675038d Keep topology source failures recoverable 2026-08-01 21:39:13 +08:00
Logic 1f87fc6082 Preserve exact entity trace handoffs 2026-08-01 20:54:19 +08:00
Logic 9200659b20 Restore initial object store setup 2026-08-01 19:55:55 +08:00
Logic 525988018f Accept empty optional status feedback 2026-08-01 19:37:22 +08:00
Logic 45e9b13f41 Guard alert rule editor for read-only roles 2026-08-01 19:14:20 +08:00
Logic 990c352308 Restore token page function ceiling 2026-08-01 18:33:09 +08:00
Logic 353d466389 Hide alert token actions from read-only roles 2026-08-01 18:29:37 +08:00
Logic 3f7b5f5359 Preserve trace storage failure state 2026-08-01 18:07:53 +08:00
Logic a17336e730 Preserve alert token return context 2026-08-01 16:43:31 +08:00
Logic cb6b967a15 Expose entity operational handoffs 2026-08-01 16:10:23 +08:00
Logic 550b60b3c0 Preserve entity import return context 2026-08-01 14:39:42 +08:00
Logic 09d613952d Connect entity discovery to monitor binding 2026-08-01 14:24:45 +08:00
Logic 062495893a Align entity list pagination contract 2026-08-01 13:49:20 +08:00
Logic a56a5be59f Align Prometheus alert ingestion and paging 2026-08-01 13:20:07 +08:00
Logic eb11374a85 Align monitor timestamp fixture pagination 2026-08-01 11:59:33 +08:00
Logic fa1f81724f Align monitor list pagination contract 2026-08-01 10:35:37 +08:00
Logic fd25899099 Allow cold monitor detection to complete 2026-08-01 10:07:44 +08:00
Logic 2c46df5690 Keep internal frontend contracts private 2026-08-01 02:14:55 +08:00
Logic 3495fa5a9f Complete scoped three-signal demo links 2026-08-01 01:41:39 +08:00
Logic cd0cffbc69 fix(explore): align stable log paging and demo links 2026-08-01 01:02:20 +08:00
Logic 3385e431f9 Merge shutdown and response handling repairs 2026-08-01 00:14:28 +08:00
Logic 809d4d8e72 Align monitor import page test with tasks 2026-08-01 00:12:57 +08:00
Logic d03e3ccba3 Classify committed response write failures 2026-08-01 00:07:49 +08:00
Logic 982adbe55b Stop window aggregator without shutdown delay 2026-07-31 23:56:07 +08:00
Logic 3cc0265ca8 Refactor monitor import task validation 2026-07-31 23:51:52 +08:00
Logic 9a751539b8 Stop service discovery worker on interruption 2026-07-31 23:48:37 +08:00
Logic 1c90fe0aa0 Merge stable shared page response contracts 2026-07-31 23:32:28 +08:00
Logic c13bdaa805 Refactor monitor import controller 2026-07-31 23:30:45 +08:00
Logic fb18213539 Consolidate stable page responses 2026-07-31 23:27:15 +08:00
Logic f11d40e07b Stabilize alert group page responses 2026-07-31 23:24:17 +08:00
Logic 1cd6764bb6 Merge stable log query response contract 2026-07-31 23:11:34 +08:00
Logic f7fb8aba16 Stabilize log query page responses 2026-07-31 23:06:46 +08:00
Logic e886a69ed0 Merge M7 runtime contract repairs 2026-07-31 22:55:00 +08:00
Logic 145080334f Prime log SSE connections on registration 2026-07-31 22:54:13 +08:00
Logic bfa9efbbd5 Stabilize monitor and entity page responses 2026-07-31 22:45:57 +08:00
Logic 672348eab6 Handle closed SSE responses safely 2026-07-31 22:32:10 +08:00
Logic d36eca9ccd Unblock the frontend production build 2026-07-31 22:13:41 +08:00
Logic 461bcb5521 Merge invalid import content classification 2026-07-31 21:59:16 +08:00
Logic 4e2a2be702 Converge fast monitor import completions 2026-07-31 21:58:07 +08:00
Logic db453cd291 Converge monitor lists after import completion 2026-07-31 21:54:32 +08:00
Logic 100fbf4140 Classify invalid monitor import content 2026-07-31 21:51:19 +08:00
Logic 6dfdd01378 Track monitor imports with canonical tasks 2026-07-31 21:46:57 +08:00
Logic 24a24a73fb Merge canonical monitor import task flow 2026-07-31 21:23:08 +08:00
Logic 2852eb645f Restore Bulletin metric refresh controls 2026-07-31 21:14:06 +08:00
Logic 611696bd4c Remove import identifiers from manager events 2026-07-31 21:12:02 +08:00
Logic 49ac99c19f Converge monitor import task state 2026-07-31 21:04:40 +08:00
Logic d5edfb7049 Preserve topology entity return context 2026-07-31 20:37:16 +08:00
Logic 3e34c35849 Distinguish entity discovery read failures 2026-07-31 20:29:49 +08:00
Logic 39ff83ad97 Align monitor definition write convergence 2026-07-31 20:17:33 +08:00
Logic ed36ecd116 Align configuration response contracts 2026-07-31 20:01:41 +08:00
Logic 12aec3d790 Converge sessions on foreground resume 2026-07-31 19:44:17 +08:00
Logic b9b1114704 Merge unauthenticated guide correction 2026-07-31 16:04:39 +08:00
Logic 1c638f046e Honor instrumentation intake authentication 2026-07-31 16:03:34 +08:00
Logic 18907eb20d Keep unauthenticated Go eBPF guides header-free 2026-07-31 15:43:04 +08:00
Logic ba3559c81a Merge instrumentation onboarding contract 2026-07-31 15:32:37 +08:00
Logic 02ae598c08 Model instrumentation intake authentication 2026-07-31 15:31:00 +08:00
Logic a2668acbf1 Align instrumentation catalog maturity 2026-07-31 15:19:40 +08:00
Logic e400ea9394 Correct Java instrumentation launch paths 2026-07-31 15:15:39 +08:00
Logic 2f7659bb53 Make Collector source disclosure reliable 2026-07-31 10:04:40 +08:00
Logic a7712a4613 Integrate alert matcher whitespace support 2026-07-31 08:57:01 +08:00
Logic 821774205a Show Collector source convergence 2026-07-31 08:56:30 +08:00
Logic 085d822377 Accept formatted alert matcher arguments 2026-07-31 08:54:43 +08:00
Logic 195683e8a7 Stabilize frontend release test execution 2026-07-30 23:47:37 +08:00
Logic 6de31a94c2 Integrate persistent exact-scope metric inventory 2026-07-30 18:25:07 +08:00
Logic 9d3b9208c8 Discover exact-scope metrics from storage inventory 2026-07-30 18:24:49 +08:00
Logic 8cae3e2279 Preserve scoped signal queries on submit 2026-07-30 17:51:40 +08:00
Logic 075472af6c Integrate safe config revision migration 2026-07-30 17:13:49 +08:00
Logic 6d202bd8b9 Preserve legacy config rows during revision migration 2026-07-30 17:13:41 +08:00
Logic 0dbbffce6d Integrate scoped metric candidate ordering 2026-07-30 16:42:45 +08:00
Logic eac36abc1f Prioritize scoped metric discovery 2026-07-30 16:42:32 +08:00
Logic 96a2da5971 Integrate authenticated internal OTLP gateway 2026-07-30 16:00:06 +08:00
Logic 5cef1c66cb Support authenticated internal OTLP gateway 2026-07-30 15:59:39 +08:00
Logic 395e71d619 Integrate scoped metric discovery 2026-07-30 15:25:15 +08:00
Logic 6a8499e484 Resolve scoped metric discovery 2026-07-30 15:24:43 +08:00
Logic 0c75c442dd Integrate collector shutdown lifecycle 2026-07-30 14:38:29 +08:00
Logic 5ef2768e27 Harden collector package shutdown lifecycle 2026-07-30 14:35:37 +08:00
Logic 17170967ee Integrate collector package startup repair 2026-07-30 08:25:59 +08:00
Logic 0fa1d0c7ac Repair collector package launch classpath 2026-07-30 08:25:42 +08:00
Logic 2053463f0e Integrate collector release validation 2026-07-30 07:43:38 +08:00
Logic a91a871822 Narrow frontend public exports 2026-07-30 07:26:29 +08:00
Logic ca5a635833 Restore alert inhibit write admission timing 2026-07-30 07:02:25 +08:00
Logic 1f0f246359 Restore monitor selection test session context 2026-07-30 06:57:46 +08:00
Logic 49b6cd705d Clarify bulletin operation contracts 2026-07-30 06:34:05 +08:00
Logic 0f00db8568 Clarify session and graph runtime state 2026-07-30 06:21:25 +08:00
Logic 8f97826ff0 Bound explore history evidence 2026-07-30 06:06:41 +08:00
Logic b7d3a8c120 Clarify monitor operation ownership 2026-07-30 05:57:13 +08:00
Logic 2a66cf4110 Clarify monitor definition state ownership 2026-07-30 05:46:55 +08:00
Logic 6cc2e89aa7 Refine startup release content validation 2026-07-30 05:23:24 +08:00
Logic 6ec577ba90 Refine settings runtime ownership 2026-07-30 05:17:17 +08:00
Logic a8bc1ee3f7 Clarify alert test contracts 2026-07-30 05:09:42 +08:00
Logic 3903841de2 Refine alert production lint boundaries 2026-07-30 04:56:36 +08:00
Logic 9d9ae8e22a Clarify status transaction boundaries 2026-07-30 04:27:45 +08:00
Logic cc243303fa Refine settings feature boundaries 2026-07-30 04:15:08 +08:00
Logic ac2e6fd69b Clarify monitor operation ownership 2026-07-30 04:01:40 +08:00
Logic 83c586505d Refine alert and live log lifecycles 2026-07-30 03:51:07 +08:00
Logic f0630df178 Refine bulletin and instrumentation boundaries 2026-07-30 03:36:48 +08:00
Logic 3f5697cafa Integrate hybrid collector backend 2026-07-30 03:18:47 +08:00
Logic 29c1eecc7c Expose signal-specific Explore filters 2026-07-30 02:46:43 +08:00
Logic aeaa8557ea Gate Nightly collector image publication 2026-07-30 02:34:28 +08:00
Logic b2930b6c8b Converge Explore query context 2026-07-30 02:20:22 +08:00
Logic a44a67a48b Align live and historical log visibility 2026-07-30 02:07:48 +08:00
Logic bcd2496a57 Scope trace queries to verified context 2026-07-30 01:57:32 +08:00
Logic 1a75e298e4 Page Entity monitors from complete evidence 2026-07-30 01:18:18 +08:00
Logic aa91a75c78 Bound Entity evidence and redact read failures 2026-07-30 00:56:03 +08:00
Logic dbd98e2e21 Converge message server updates by revision 2026-07-30 00:36:13 +08:00
Logic d3e09687f6 Prove Entity route authorization roles 2026-07-30 00:08:07 +08:00
Logic 8ce6ca0479 Separate Explore result orchestration 2026-07-29 23:58:50 +08:00
Logic 458801887e Guard message server updates with revisions 2026-07-29 23:51:12 +08:00
Logic 0b26c372c4 Keep Label permissions behind controller boundary 2026-07-29 23:35:18 +08:00
Logic f232cefe60 Publish Hybrid Collector Nightly with Java 25 2026-07-29 23:15:27 +08:00
Logic b823f4f84a Keep Explore refresh evidence honest 2026-07-29 23:14:57 +08:00
Logic 44f8b33912 Prove authenticated three-signal public APIs 2026-07-29 22:43:14 +08:00
Logic dbc6c42a20 Adapt Sureness responses for Spring Framework 7 2026-07-29 22:39:49 +08:00
Logic ce7d99fa4f Retire onboarding context after manual queries 2026-07-29 21:44:32 +08:00
Logic 2fd47ec212 Refresh sessions before access expiry 2026-07-29 21:30:04 +08:00
Logic d12e02f889 Harden live log workspace ownership 2026-07-29 21:27:01 +08:00
Logic fb8a88841b Restore Dashboard label drilldown 2026-07-29 20:51:12 +08:00
Logic 4db5f64d40 Allow safe exit from Bulletin recovery 2026-07-29 20:20:39 +08:00
Logic e1308e4000 Recover Bulletin pagination from authoritative pages 2026-07-29 19:27:18 +08:00
Logic e5e9cf7ae1 Honor monitor definition deep links 2026-07-29 18:46:07 +08:00
Logic b61bb4b147 Prove ambiguous monitor definition writes 2026-07-29 17:56:11 +08:00
Logic 32e87159ad Retire unauthorized monitor definition writes 2026-07-29 16:55:46 +08:00
Logic 1ac120fb8d Retire unauthorized Collector mutations 2026-07-29 16:02:03 +08:00
Logic 820aaadca1 Restore Collector deployment guidance 2026-07-29 15:39:15 +08:00
Logic f7e48c5e5e Converge session caches across browser tabs 2026-07-29 14:50:00 +08:00
Logic 3615d64916 Preserve trace operation context in metrics 2026-07-29 14:18:32 +08:00
Logic fc53c3e13b Consume external alert integration catalog 2026-07-29 13:56:23 +08:00
Logic 4687d9edc6 Restore recent firing alerts on Dashboard 2026-07-29 13:18:00 +08:00
Logic 9e3dbd6bc5 Move alert template modules into layers 2026-07-29 13:17:23 +08:00
Logic 64a4c7fcd7 Reject invalid external alert batches 2026-07-29 12:56:25 +08:00
Logic 4945f457c3 Publish external alert integration guides 2026-07-29 12:35:15 +08:00
Logic cc57f0ae50 Swap topology graphs after render 2026-07-29 12:19:38 +08:00
Logic 6a3ee98f84 Expose unavailable trace topology evidence 2026-07-29 12:00:50 +08:00
Logic 1192fd2e54 Restore Dashboard summary evidence 2026-07-29 11:46:27 +08:00
Logic a4ef587756 Scope topology seeds by environment 2026-07-29 11:41:37 +08:00
Logic 378009249a Restore Dashboard Collector evidence 2026-07-29 11:28:55 +08:00
Logic e8f15a2c4b Reject invalid topology source kinds 2026-07-29 11:16:45 +08:00
Logic 4db2d80a2c Scan Collector container release layers 2026-07-29 11:01:39 +08:00
Logic 05f1e81db2 Localize monitor application catalogs 2026-07-29 10:53:32 +08:00
Logic 22d75b477c Scan CycloneDX root components 2026-07-29 10:35:25 +08:00
Logic e3b2785413 Scan embedded runtime SBOM components 2026-07-29 10:30:32 +08:00
Logic d1f9ccecaa Restore canonical legacy route redirects 2026-07-29 10:24:11 +08:00
Logic 9082473005 Constrain JVM Collector application artifact 2026-07-29 10:14:41 +08:00
Logic 03730e3d19 Classify Alert Group rejection evidence 2026-07-29 10:06:40 +08:00
Logic 31b5b6d13e Preserve instrumentation service context 2026-07-29 09:56:10 +08:00
Logic 441d6c5692 Verify JVM Hybrid runtime assets 2026-07-29 09:51:24 +08:00
Logic e28c798329 Verify runtime SBOM target metadata 2026-07-29 09:26:45 +08:00
Logic 05eb83e167 Align Label actions with role capabilities 2026-07-29 09:22:16 +08:00
Logic 1015cf5024 Reject renamed native release payloads 2026-07-29 09:16:38 +08:00
Logic 39f729ed5f Gate nightly Collector publication 2026-07-29 08:59:05 +08:00
Logic 36437b7705 Retire status operations on access changes 2026-07-29 08:46:58 +08:00
Logic 6853180936 Isolate direct telemetry detection 2026-07-29 08:44:22 +08:00
Logic ac41bf243d Define Collector route access 2026-07-29 08:20:46 +08:00
Logic 03649afa9d Preserve Bulletin field order 2026-07-29 08:14:45 +08:00
Logic c8b3d98ee7 Report Greptime runtime availability accurately 2026-07-29 08:10:25 +08:00
Logic 0b5bae214e Enforce global mute access 2026-07-29 07:59:15 +08:00
Logic 93d7b59a6b Define general configuration access 2026-07-29 07:29:57 +08:00
Logic 71494d0819 Restore public status evidence 2026-07-29 07:26:07 +08:00
Logic 09b6fc99ad Align Label authorization resources 2026-07-29 07:06:36 +08:00
Logic d920f02ab2 Recover ambiguous system configuration saves 2026-07-29 06:46:22 +08:00
Logic 4f8a396f3f Enforce monitor read permissions 2026-07-29 06:22:56 +08:00
Logic d44556e889 Retire unauthorized monitor imports 2026-07-29 05:58:17 +08:00
Logic 0a7f1882ce Retire unauthorized monitor exports 2026-07-29 05:37:18 +08:00
Logic 11dc9136ca Constrain Collector Runtime gRPC dependency 2026-07-29 05:31:00 +08:00
Logic 16c9459b51 Retire unauthorized Grafana deletion 2026-07-29 05:26:02 +08:00
Logic a34f3fd5ac Retire unauthorized monitor list commands 2026-07-29 05:15:08 +08:00
Logic 072bc67352 Classify bulletin permission failures 2026-07-29 04:59:50 +08:00
Logic ef2a11a594 Apply system configuration after commit 2026-07-29 04:44:55 +08:00
Logic 52f0cdaad3 Present bulletin actions by role 2026-07-29 04:42:45 +08:00
Logic 4d82f05845 Enforce bulletin action permissions 2026-07-29 04:34:28 +08:00
Logic 86f2a14a1a Secure object store configuration lifecycle 2026-07-29 04:13:57 +08:00
Logic 393dc20d65 Enforce instrumentation token permissions 2026-07-29 03:55:59 +08:00
Logic 413fabf3cd Align collector guides with scoped detection 2026-07-29 03:42:15 +08:00
Logic afbf10710e Hide object store writes from non-writers 2026-07-29 03:33:42 +08:00
Logic 2a6bdff636 Complete push monitor lifecycle 2026-07-29 03:16:35 +08:00
Logic 07a7602383 Route auto-created push monitor catalogs 2026-07-29 03:14:55 +08:00
Logic d62ace8ddf Converge status management writes 2026-07-29 03:07:17 +08:00
Logic 70032cedc5 Show monitor parameter evidence safely 2026-07-29 02:37:28 +08:00
Logic 43d5e7ff99 Preserve monitor schedules across dispatch 2026-07-29 02:33:47 +08:00
Logic 47e0ec3961 Complete plugin configuration lifecycle 2026-07-29 02:12:19 +08:00
Logic 64f1d02a70 Enforce collector management permissions 2026-07-29 02:01:27 +08:00
Logic 4318eea29d Enforce status management permissions 2026-07-29 01:34:53 +08:00
Logic 3a9c197f8f Export runtime telemetry directly 2026-07-29 00:50:38 +08:00
Logic 4ae325cae8 Bound OTLP exporter requests 2026-07-29 00:25:41 +08:00
Logic 1380d6fdc8 Separate direct filters from onboarding scope 2026-07-29 00:21:00 +08:00
Logic da5b134efb Complete live log recovery 2026-07-28 23:52:13 +08:00
Logic 83235c5cfc Harden live log streaming 2026-07-28 23:32:53 +08:00
Logic 2da42f54cf Use ID-scoped alert operation proof 2026-07-28 22:04:10 +08:00
Logic a0cf97c85b Add ID-scoped alert evidence 2026-07-28 22:03:47 +08:00
Logic 9328b9bc8c Complete alert center lifecycle refresh 2026-07-28 21:36:40 +08:00
Logic 815caffbdd Publish committed alert lifecycle events 2026-07-28 21:36:22 +08:00
Logic 32d40e71a4 Complete alert rule editor parity 2026-07-28 20:42:59 +08:00
Logic 98861b9961 Complete alert rule lifecycle parity 2026-07-28 20:21:45 +08:00
Logic 3995f7117e Canonicalize alert list query state 2026-07-28 19:25:55 +08:00
Logic 2e5fbbd83b Preserve custom template search scope 2026-07-28 19:18:04 +08:00
Logic 2dac5cb5f5 Preserve dashboard source failure states 2026-07-28 19:03:25 +08:00
Logic 46ac63fd6e Harden dashboard read failure boundaries 2026-07-28 19:01:00 +08:00
Logic 7c7cd4ae78 Distinguish signal query permission failures 2026-07-28 18:48:14 +08:00
Logic f41793fd86 Align telemetry query reader permissions 2026-07-28 18:43:10 +08:00
Logic c4d23753cc Align alert silence action permissions 2026-07-28 18:34:35 +08:00
Logic ba9b9d1a8d Classify null alert silence updates safely 2026-07-28 18:21:33 +08:00
Logic 2b6b9ad795 Align alert inhibit action permissions 2026-07-28 18:13:32 +08:00
Logic 8ed36b0a9e Harden alert inhibit management contract 2026-07-28 17:54:02 +08:00
Logic bba094fb3c Align alert group action permissions 2026-07-28 17:26:23 +08:00
Logic c8e181506e Align alert rule action permissions 2026-07-28 16:59:15 +08:00
Logic 1b18140cc4 Preserve topology state across edge pages 2026-07-28 16:28:19 +08:00
Logic 2aa0a0229c Retire denied navigation on role changes 2026-07-28 16:15:04 +08:00
Logic 00c98a59c9 Make plaintext telemetrygen guides safe 2026-07-28 15:52:20 +08:00
Logic c1c45e8005 Retire stale explore query context 2026-07-28 15:25:17 +08:00
Logic 216fdb34d0 Harden instrumentation detection context 2026-07-28 15:05:25 +08:00
Logic 5bb30f7b96 Complete entity write authorization workflows 2026-07-28 14:38:13 +08:00
Logic bf05b5492f Align entity list and detail contracts 2026-07-28 13:39:33 +08:00
Logic 2a76b9cd24 Define entity route authorization 2026-07-28 13:35:41 +08:00
Logic aa964191da Fix native collector runtime root 2026-07-28 13:27:32 +08:00
Logic 9b5d772b24 Align label management contract 2026-07-28 12:42:30 +08:00
Logic 8a03da0caf Align plugin management contract 2026-07-28 11:59:36 +08:00
Logic 1dbad6aa20 Align object storage response contract 2026-07-28 10:56:19 +08:00
Logic a3894a1aa7 Align API token response contract 2026-07-28 10:22:40 +08:00
Logic 253fac8b0e fix: harden plugin management lifecycle 2026-07-28 10:12:08 +08:00
Logic f30242106d Extract message server draft update 2026-07-28 10:03:55 +08:00
Logic cddfdcba08 Align system settings contract 2026-07-28 09:54:46 +08:00
Logic 2c0a215bcd fix: harden API token lifecycle 2026-07-28 09:45:31 +08:00
Logic 066ab9641c Guard message server configuration actions 2026-07-28 09:24:47 +08:00
Logic b1c884bb38 fix: protect object store configuration 2026-07-28 09:16:56 +08:00
Logic bc099d8579 fix: harden system configuration contract 2026-07-28 08:57:21 +08:00
Logic f87f717bd5 Align message server response contract 2026-07-28 08:53:08 +08:00
Logic 3cf8c895ed Retire unauthorized notice template operations 2026-07-28 08:35:21 +08:00
Logic fca7a3d242 fix: protect message server configuration writes 2026-07-28 08:29:51 +08:00
Logic 26a0d5124e Guard notice template commands 2026-07-28 08:12:17 +08:00
Logic 9b6c7676f6 fix: enforce notice template mutation boundaries 2026-07-28 08:07:50 +08:00
Logic cf71677276 Retire unauthorized notice rule operations 2026-07-28 07:52:09 +08:00
Logic da2acd59ca Guard notice rule command permissions 2026-07-28 07:21:04 +08:00
Logic 07e5ebcf19 Enforce notice receiver action permissions 2026-07-28 07:00:59 +08:00
Logic 6c41b7d88d Enforce alert center action permissions 2026-07-28 06:18:09 +08:00
Logic 341140aea6 fix: accept yml monitor imports 2026-07-28 05:39:43 +08:00
Logic 47f0e31a3e fix: reconcile failed monitor copies 2026-07-28 05:32:52 +08:00
Logic 10dd9b2785 fix: enforce monitor detail permissions 2026-07-28 05:23:15 +08:00
Logic a5878f96ac fix: validate alert group status 2026-07-28 05:12:59 +08:00
Logic 4c90f9b5e0 fix: reject partial alert group deletes 2026-07-28 05:05:50 +08:00
Logic f581a50d68 fix: reject partial alert status updates 2026-07-28 04:59:51 +08:00
Logic d98c44f4b0 fix: enforce monitor editor permissions 2026-07-28 04:55:17 +08:00
Logic 3ac2a593be fix: make monitor copy failures safe 2026-07-28 04:48:38 +08:00
Logic 2ccd504245 fix: align monitor list permissions 2026-07-28 04:36:44 +08:00
Logic 116825a97a fix: restore monitor yaml import 2026-07-28 04:31:23 +08:00
Logic 19ba6ea2d6 fix: align monitor copy permission 2026-07-28 04:07:52 +08:00
Logic 397e9924de fix: preserve runtime status evidence 2026-07-28 03:44:27 +08:00
Logic 0411380f89 fix: prove collector runtime application 2026-07-28 03:20:57 +08:00
Logic cb2a3286fc fix: reconcile uncertain monitor definition writes 2026-07-28 01:50:56 +08:00
Logic e6b5835717 maintenance: validate shell resource metadata 2026-07-28 01:37:41 +08:00
Logic 1079d3b695 fix: require monitor definition YAML locally 2026-07-28 01:07:39 +08:00
Logic 3c75de82f7 fix: require explicit collector intake URLs 2026-07-28 00:54:38 +08:00
Logic f5dce5fdbf fix: align collector intake and runtime drafts 2026-07-28 00:50:06 +08:00
Logic 9b0ff8e138 fix: converge alert silence mutations 2026-07-28 00:32:49 +08:00
Logic 2f68bc51f7 fix(alert): prove inhibit create identity 2026-07-28 00:07:48 +08:00
Logic 2601de3535 fix(router): align legacy redirect boundaries 2026-07-27 23:45:19 +08:00
Logic dad7788e5c feat(startup): add configured external intake profile 2026-07-27 23:35:20 +08:00
Logic 148e3021b1 fix(alert): validate notification rule dependencies 2026-07-27 23:28:00 +08:00
Logic bddb09fc2c feat(auth): enforce administrative route access 2026-07-27 23:09:16 +08:00
Logic 3d096ffb0e maintenance(runtime): align manager stream access 2026-07-27 22:45:21 +08:00
Logic 96dcdf7643 maintenance(runtime): align alert stream access 2026-07-27 22:40:22 +08:00
Logic d6c193e1cc maintenance(runtime): align log stream access 2026-07-27 22:34:07 +08:00
Logic 24cfe10ffe fix(release): restore safe packaged lifecycle 2026-07-27 21:54:37 +08:00
Logic 359d11f35a build(java): keep server modules on Java 25 2026-07-27 21:35:23 +08:00
Logic 7a68833f86 fix(release): align Collector image with Java 25 2026-07-27 21:12:58 +08:00
Logic b8202e38d3 fix(runtime): close SSE streams during shutdown 2026-07-27 21:02:15 +08:00
Logic d51fd7f600 fix(entity): classify current missing envelopes 2026-07-27 20:50:05 +08:00
Logic 661b745962 fix(release): serve packaged React deep routes 2026-07-27 20:31:53 +08:00
Logic aa0bbaea56 test(e2e): tolerate partial Kafka setup 2026-07-27 20:14:13 +08:00
Logic c42f0d5a56 test(manager): align bounded entity query contracts 2026-07-27 19:52:57 +08:00
Logic b7822a1753 test(observability): align request context ownership 2026-07-27 19:33:39 +08:00
Logic 4509f4b6d2 merge: record validated collector maintenance 2026-07-27 19:21:32 +08:00
Logic 15cb528c02 docs(instrumentation): align catalog source links 2026-07-27 18:48:59 +08:00
Logic 7f39c67eab maintenance(runtime): update text dependency 2026-07-27 18:36:36 +08:00
Logic 83fc231b3d maintenance(release): align packaged web runtime 2026-07-27 18:17:56 +08:00
Logic 5cbf3982d0 maintenance(frontend): remove standalone web image 2026-07-27 16:44:53 +08:00
Logic a741b8a8d2 maintenance(frontend): switch release entrypoints to web-app 2026-07-27 14:32:26 +08:00
Logic a93ca81d4c maintenance(frontend): remove deprecated web-next 2026-07-27 14:03:38 +08:00
Logic b47aee0bce maintenance(runtime): harden recovery boundaries 2026-07-27 13:20:40 +08:00
Logic dbd84945e3 test(collector): reuse verified agent cache 2026-07-27 13:00:26 +08:00
Logic c92d2d2224 merge: integrate hybrid collector work 2026-07-27 11:45:22 +08:00
Logic a698b030d9 test(monitors): await route canonicalization 2026-07-27 11:18:18 +08:00
Logic ff99b45b20 test(labels): isolate page ownership checks 2026-07-27 10:49:20 +08:00
Logic 88391d0a1f test(alerts): isolate bulk operation flows 2026-07-27 10:16:10 +08:00
Logic 585943d912 refactor(types): narrow unused exports 2026-07-27 09:47:58 +08:00
Logic d3fddf862f test(routes): align ownership harnesses 2026-07-27 09:22:01 +08:00
Logic 4a171b628c refactor(frontend): clear typecheck contracts 2026-07-27 08:52:04 +08:00
Logic 729b9b3221 refactor(alerts): honor exact optional contracts 2026-07-27 08:36:43 +08:00
Logic 7845091a5c refactor(topology): clarify completeness invariants 2026-07-27 08:18:54 +08:00
Logic ba7589d733 refactor(monitors): isolate metric query options 2026-07-27 08:04:51 +08:00
Logic 4ad8072070 refactor(alerts): isolate binding sessions 2026-07-27 07:52:05 +08:00
Logic aff9f22d77 refactor(alerts): simplify model parsers 2026-07-27 07:34:46 +08:00
Logic ddc36e93a2 refactor(alerts): clear lint and type hygiene 2026-07-27 07:16:54 +08:00
Logic 72e4ad49df refactor(topology): split evidence presentation duties 2026-07-27 06:55:43 +08:00
Logic 5e07790ffa refactor(labels): split page workspace duties 2026-07-27 06:45:48 +08:00
Logic 45b0951020 refactor(alerts): split presentation duties 2026-07-27 06:36:07 +08:00
Logic 88e83067ed refactor(alerts): split editor and import duties 2026-07-27 06:19:57 +08:00
Logic ff03c32842 refactor(monitors): split list and metric concerns 2026-07-27 06:02:09 +08:00
Logic cd2670b99e refactor(bulletin): split page and transaction duties 2026-07-27 05:46:23 +08:00
Logic 56953edd04 refactor(alerts): enforce model dependency boundaries 2026-07-27 05:32:20 +08:00
Logic 087359e170 refactor(object-store): share operational page 2026-07-27 05:14:01 +08:00
Logic c0f2f96c8e refactor(system-config): share operational page 2026-07-27 05:09:56 +08:00
Logic 439d97a2a4 refactor(labels): share operational page actions 2026-07-27 05:05:44 +08:00
Logic 71613f02d2 refactor(tokens): share operational page header 2026-07-27 04:59:29 +08:00
Logic d6d7d951ee refactor(message-server): share operational page 2026-07-27 04:53:08 +08:00
Logic defedf5157 refactor(plugins): share operational page structure 2026-07-27 04:49:10 +08:00
Logic 448bb2ee71 refactor(monitor-definitions): share operational page 2026-07-27 04:44:05 +08:00
Logic 4a0c750273 refactor(collectors): share operational page structure 2026-07-27 04:35:11 +08:00
Logic 1437931f4f fix(shell): theme owned scrollbars 2026-07-27 04:26:09 +08:00
Logic 13e1965450 feat(monitors): separate management and filter actions 2026-07-27 04:18:00 +08:00
Logic eb4ed35c88 feat(ui): share operational page structure 2026-07-27 04:01:44 +08:00
Logic 3d91d86402 fix(explore): keep live failure states honest 2026-07-27 03:35:02 +08:00
Logic 63b5a7fe88 feat(topology): consume graph completeness evidence 2026-07-27 03:27:01 +08:00
Logic 432c11a5c4 feat(topology): disclose partial graph results 2026-07-27 03:06:07 +08:00
Logic 1c53f1113d feat(alerts): restore source setup guidance 2026-07-27 02:40:29 +08:00
Logic 0f0eb33fff feat(monitors): preserve generic create return 2026-07-27 02:23:49 +08:00
Logic e2ad1b7ccb feat(monitors): refresh authoritative detail 2026-07-27 02:11:35 +08:00
Logic 8d27247919 test: prove runtime rebuild cleanup 2026-07-27 02:02:19 +08:00
Logic bbc7731258 feat(monitors): favorite realtime-only groups 2026-07-27 02:02:04 +08:00
Logic f2df7dc9ee test: prove persistent queue corruption convergence 2026-07-27 01:55:19 +08:00
Logic efd80f72da feat(monitors): preserve realtime collection time 2026-07-27 01:53:54 +08:00
Logic de20f45917 test: prove persistent queue recovery after runtime restart 2026-07-27 01:44:54 +08:00
Logic 744c590139 feat(monitors): add long history ranges 2026-07-27 01:43:43 +08:00
Logic 078c8158ab feat(monitors): expose realtime-only metric groups 2026-07-27 01:32:23 +08:00
Logic 802480b9a4 maintenance: converge exporter unavailable status 2026-07-27 01:27:53 +08:00
Logic d577b1084d feat(monitors): preserve realtime group fields 2026-07-27 01:21:13 +08:00
Logic 4f2a999a49 feat(monitors): confirm state changes 2026-07-27 01:01:09 +08:00
Logic f8354b087e maintenance: stop reset fixture on accept failure 2026-07-27 00:59:44 +08:00
Logic 3ec4ef0f4b test: prove exporter reset convergence 2026-07-27 00:50:33 +08:00
Logic 91f859e9e6 maintenance: converge exporter rate limit status 2026-07-27 00:34:01 +08:00
Logic 8edba786c6 maintenance: remove stale port adjustment notice 2026-07-27 00:03:03 +08:00
Logic fe21b49bac test: prove exporter queue capacity convergence 2026-07-27 00:02:49 +08:00
Logic 7d25fb40c5 feat(monitors): align secure default ports 2026-07-26 23:07:25 +08:00
Logic 04d34e7380 maintenance: pause hidden monitor refresh 2026-07-26 22:26:59 +08:00
Logic a17127f0ee feat(monitors): retain disappeared rows briefly 2026-07-26 22:02:02 +08:00
Logic db27099133 maintenance: converge exporter authentication status 2026-07-26 21:52:37 +08:00
Logic 1deaaf0def maintenance: retire stale binding sessions 2026-07-26 21:10:02 +08:00
Logic a00ae01470 feat(alerts): manage metric monitor bindings 2026-07-26 21:02:54 +08:00
Logic 69721a9588 maintenance: converge runtime port conflicts 2026-07-26 20:55:00 +08:00
Logic 6b989c22fc feat(alerts): own monitor binding contracts 2026-07-26 20:25:14 +08:00
Logic 1f077e0f15 feat(alerts): restore structured metric conditions 2026-07-26 20:11:29 +08:00
Logic cfcf8ba09f feat(alerts): edit structured metric conditions 2026-07-26 19:21:24 +08:00
Logic b200e31945 refactor(alerts): split metric target evidence 2026-07-26 19:16:54 +08:00
Logic f0b8f16a50 feat(alerts): restore metric target controls 2026-07-26 19:14:19 +08:00
Logic 7adff98a59 feat(alerts): own metric authoring draft 2026-07-26 18:47:33 +08:00
Logic 686b9f84a3 feat(alerts): load metric target hierarchy 2026-07-26 18:35:19 +08:00
Logic cf13970f7b feat(alerts): project metric target catalog 2026-07-26 18:26:06 +08:00
Logic 9f32228e21 feat(alerts): restore structured metric conditions 2026-07-26 18:21:33 +08:00
Logic 27a6a5c1aa feat(alerts): restore metric target expressions 2026-07-26 18:13:07 +08:00
Logic b22d25a177 feat(alerts): restore strategy transitions 2026-07-26 18:03:05 +08:00
Logic 34a1e4fdea feat(alerts): restore datasource capability gating 2026-07-26 17:57:31 +08:00
Logic 552e5b754b feat(bulletin): restore batch deletion 2026-07-26 17:37:41 +08:00
Logic a8034d155f feat(alerts): restore rule import 2026-07-26 17:18:05 +08:00
Logic 313b3f2f64 feat(alerts): restore rule export 2026-07-26 17:01:36 +08:00
Logic 70bc27626b feat(alerts): restore rule batch deletion 2026-07-26 16:46:08 +08:00
Logic ae25e8bcee feat(alerts): restore silence batch deletion 2026-07-26 16:18:17 +08:00
Logic bb268e4a1d chore(frontend): trim unused public exports 2026-07-26 15:37:53 +08:00
Logic 9f14bb1cf1 feat(monitors): restore dynamic navigation 2026-07-26 15:30:30 +08:00
Logic 628f29a275 feat(alerts): restore acknowledge workflow 2026-07-26 14:34:30 +08:00
Logic 3c6c4b8784 feat(shell): restore fullscreen action 2026-07-26 13:49:53 +08:00
Logic 3917e0433d feat(monitor): restore detail metadata 2026-07-26 13:25:30 +08:00
Logic 4e2c33b35d feat(shell): restore account actions 2026-07-26 13:00:33 +08:00
Logic 9a9395fdc6 feat(alerts): restore batch alert operations 2026-07-26 12:28:29 +08:00
Logic 34ead39640 feat(monitors): restore import task notifications 2026-07-26 11:57:05 +08:00
Logic 61f34ef3e4 feat(shell): restore alert notification controls 2026-07-26 11:33:42 +08:00
Logic 895be9e8dd feat(shell): restore alert notifications 2026-07-26 10:53:26 +08:00
Logic b56a4efc90 feat(alerts): restore server label suggestions 2026-07-26 10:18:12 +08:00
Logic e3e78985d0 feat(alert-inhibits): restore entity alert prefill 2026-07-26 09:54:03 +08:00
Logic 567aa9978c feat(entities): restore noise control handoff 2026-07-26 09:18:57 +08:00
Logic 17f8baf0db feat(alert-silences): restore entity matched view 2026-07-26 09:06:32 +08:00
Logic a2c6331d35 feat(alert-inhibits): restore entity matched view 2026-07-26 08:45:41 +08:00
Logic 5c0f9883f1 feat(entities): restore noise-control evidence 2026-07-26 08:14:43 +08:00
Logic f1667843e8 feat(alert-inhibits): restore batch deletion 2026-07-26 07:57:57 +08:00
Logic a6078ea0ec feat(alert-groups): restore batch deletion 2026-07-26 07:27:46 +08:00
Logic 7244c0adc6 feat(alerts): restore child alert evidence 2026-07-26 06:48:21 +08:00
Logic 1da38a783a fix(alerts): correct delete locale scope 2026-07-26 06:32:51 +08:00
Logic 6c5feaf342 feat(alerts): restore realtime refresh 2026-07-26 06:22:38 +08:00
Logic 20c1016b27 feat(alerts): restore group deletion 2026-07-26 06:09:00 +08:00
Logic 82a72d563e feat(alerts): recover invalid result pages 2026-07-26 05:41:13 +08:00
Logic c73d03ac61 feat(notice-templates): recover invalid result pages 2026-07-26 05:35:01 +08:00
Logic cd5ae52b9f feat(notice-rules): recover invalid result pages 2026-07-26 05:26:37 +08:00
Logic d696193065 feat(receivers): recover invalid result pages 2026-07-26 05:17:00 +08:00
Logic 09c766c945 feat(monitors): recover invalid result pages 2026-07-26 05:05:51 +08:00
Logic 42e7f27e2a feat(monitors): restore cross-page selection 2026-07-26 04:52:48 +08:00
Logic e875937aa9 maintenance: correct Grafana dashboard delete response 2026-07-26 04:36:28 +08:00
Logic 4f51993bda feat(monitors): restore Grafana dashboard cleanup 2026-07-26 04:30:08 +08:00
Logic d4488c4beb feat(monitors): restore label suggestions 2026-07-26 04:09:33 +08:00
Logic 572cf6fa43 feat(monitors): restore Grafana template import 2026-07-26 03:42:36 +08:00
Logic 3965765ded feat(monitors): restore row interactions 2026-07-26 03:30:15 +08:00
Logic 8b88a71c9d feat(monitors): restore labels and help links 2026-07-26 03:11:08 +08:00
Logic cac8c943b2 feat(monitors): restore favorite metric collection 2026-07-26 02:52:10 +08:00
Logic 81333e4192 feat(monitors): restore Grafana dashboard display 2026-07-26 02:25:17 +08:00
Logic 18e11b9f8c feat(monitors): restore detail refresh controls 2026-07-26 02:10:43 +08:00
Logic 6ef9730420 feat(monitors): restore list refresh and sorting 2026-07-26 01:33:35 +08:00
Logic 4e63287d3b feat(monitors): restore configuration import 2026-07-26 01:12:03 +08:00
Logic 2199d84a46 feat(monitors): restore configuration export 2026-07-25 23:56:21 +08:00
Logic b03b13ff99 fix(monitors): recognize missing detail responses 2026-07-25 22:44:51 +08:00
Logic 68a9c2f38d fix(monitors): preserve application on create 2026-07-25 22:22:15 +08:00
Logic 4273776365 merge: integrate collector runtime maintenance 2026-07-25 21:19:48 +08:00
Logic 1372bbc5f4 fix(entities): lock editor while saving 2026-07-25 21:19:11 +08:00
Logic 3d71691437 maintenance: sanitize runtime supervisor diagnostics 2026-07-25 21:18:44 +08:00
Logic 2621306f17 maintenance: normalize runtime status diagnostics 2026-07-25 20:55:09 +08:00
Logic c394a14c05 Refine instrumentation source directory controls 2026-07-25 18:55:37 +08:00
Logic fd3d6e8bcb style(observability): simplify source directory cards 2026-07-25 17:16:56 +08:00
Logic c6c1e64300 style(web): establish layered corner radii 2026-07-25 15:00:29 +08:00
Logic 0ad3e3c209 feat(observability): disclose intake transport security 2026-07-25 13:01:42 +08:00
Logic 9548d88259 feat: support explicit plaintext intake 2026-07-25 12:50:30 +08:00
Logic 94fd9494e5 style(web): soften shared control surfaces 2026-07-25 12:26:09 +08:00
Logic 47a3e2b393 feat(observability): simplify instrumentation onboarding 2026-07-25 12:19:42 +08:00
Logic b59c2c6216 feat(observability): align onboarding with source flow 2026-07-25 07:03:09 +08:00
Logic 52c7470c79 fix(observability): preserve endpoint-correlated logs 2026-07-25 07:02:51 +08:00
Logic b006972bcb chore(observability): trim instrumentation exports 2026-07-25 02:20:19 +08:00
Logic 5a6d3b220f merge: integrate hybrid collector onboarding 2026-07-25 02:05:23 +08:00
Logic 8123e8db9d fix(observability): require explicit instrumentation source 2026-07-25 02:01:11 +08:00
Logic 93d067a41d feat(observability): redesign instrumentation onboarding 2026-07-25 01:43:27 +08:00
Logic 38c365d326 fix: align managed source identity 2026-07-25 00:48:29 +08:00
Logic 16c34eee46 fix: align instrumentation guides with detection 2026-07-25 00:44:41 +08:00
Logic 233825016c feat: unify instrumentation onboarding catalog 2026-07-25 00:32:18 +08:00
Logic 62a7195702 refactor(observability): isolate instrumentation back action 2026-07-24 22:11:27 +08:00
Logic 9ccb814279 fix(observability): accept direct server explore handoffs 2026-07-24 22:01:49 +08:00
Logic 3020ec1ff8 fix(observability): freeze instrumentation detection window 2026-07-24 21:42:35 +08:00
Logic 696ea5c229 fix(observability): clarify version applicability 2026-07-24 21:33:46 +08:00
Logic b5563c98c1 merge: scope direct server signal detection
# Conflicts:
#	hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/instrumentation/store/greptime/GreptimeInstrumentationDetectionQueryFactory.java
2026-07-24 21:17:06 +08:00
Logic f6c59bdec5 fix(observability): scope direct server detection 2026-07-24 21:10:49 +08:00
Logic ab16865dcc fix(observability): widen instrumentation questions 2026-07-24 21:06:20 +08:00
Logic 487f794ef1 merge: canonicalize instrumentation catalog URLs 2026-07-24 20:46:40 +08:00
Logic 46a95a0153 fix(observability): canonicalize catalog source URLs 2026-07-24 20:45:56 +08:00
Logic dff2c2e343 merge: harden instrumentation serialization 2026-07-24 20:31:41 +08:00
Logic b4f1a49432 fix(observability): omit unset instrumentation fields 2026-07-24 20:30:45 +08:00
Logic f178ef76b8 chore(observability): narrow instrumentation exports 2026-07-24 20:10:51 +08:00
Logic ad2f064ed9 merge: align instrumentation route access 2026-07-24 19:54:01 +08:00
Logic 2f0aa2bb51 fix(observability): localize instrumentation v2 flow 2026-07-24 19:52:31 +08:00
Logic 30dc54d08a maintenance: align instrumentation route access 2026-07-24 19:46:39 +08:00
Logic e00177c0b9 merge: integrate instrumentation v2 backend 2026-07-24 19:27:39 +08:00
Logic caeacd265d fix(observability): initialize quick-start instrumentation 2026-07-24 19:25:42 +08:00
Logic 84533fe8c4 fix(observability): refine instrumentation v2 onboarding 2026-07-24 18:52:04 +08:00
Logic 5f970a7a2e fix(observability): align instrumentation source keys 2026-07-24 18:23:12 +08:00
Logic 63c6e1381e feat(observability): replace instrumentation onboarding flow 2026-07-24 18:01:04 +08:00
Logic ed455a4bbe feat(observability): define instrumentation v2 frontend contract 2026-07-24 17:44:53 +08:00
Logic 4760c42417 feat(observability): harden instrumentation v2 guidance 2026-07-24 17:22:32 +08:00
Logic 4615df9592 feat(observability): add instrumentation v2 onboarding 2026-07-24 16:46:58 +08:00
Logic 45ac52ee2c fix(web): standardize shell page width 2026-07-24 15:11:28 +08:00
Logic 4626f5f363 fix(web): keep shell navigation below header 2026-07-24 14:58:44 +08:00
Logic d42f558e6a fix(collector): anchor native runtime home 2026-07-24 12:00:41 +08:00
Logic c35edcd6ce chore(runtime): refresh transitive dependency floor 2026-07-24 10:53:37 +08:00
Logic 5304b482cc fix(manager): report Greptime runtime status honestly 2026-07-24 09:51:43 +08:00
Logic 7d1b7f557c fix(observability): align scoped Greptime detection 2026-07-24 09:47:42 +08:00
Logic a6eabc61e5 merge: integrate hybrid collector backend 2026-07-24 08:51:44 +08:00
Logic a481a39104 fix(monitor): preserve backend instance contract 2026-07-24 08:27:23 +08:00
Logic 7bff93dca6 fix(monitor): keep hidden-layout apps selectable 2026-07-24 08:13:23 +08:00
Logic 625e389920 refactor: evolve common-core payload compatibility (#4236) 2026-07-24 00:55:30 +08:00
Logic cd54603b82 refactor(topology): narrow icon type export 2026-07-24 00:14:55 +08:00
Logic eacb71a010 refactor(instrumentation): narrow scope type exports 2026-07-24 00:11:09 +08:00
Logic 05860be267 refactor(explore): colocate query patch type 2026-07-23 23:55:03 +08:00
Logic e5f9452bfc feat(dashboard): unify operational summary 2026-07-23 23:40:09 +08:00
Logic 0c17a50ac6 refactor(topology): split graph option builders 2026-07-23 23:38:03 +08:00
Logic 89798b2196 feat(topology): improve node identity visuals 2026-07-23 23:09:01 +08:00
Logic a01e66dfd6 fix(shell): constrain implicit route tracks 2026-07-23 22:12:03 +08:00
Logic 3e2d827436 fix(shell): shrink route content items 2026-07-23 22:03:48 +08:00
Logic 0a745011fd fix(shell): contain wide table overflow 2026-07-23 21:55:53 +08:00
Logic 33c885dc65 fix(explore): clear stale trace pivot context 2026-07-23 21:32:42 +08:00
Logic 2a96d55652 feat(topology): add source-backed node icons 2026-07-23 21:18:35 +08:00
Logic 4f97729bd5 feat(explore): share relative auto refresh 2026-07-23 20:58:38 +08:00
Logic 95fad6ca72 fix(collector): isolate Java Agent smoke dependency 2026-07-23 20:58:11 +08:00
Logic 7627da9b18 fix(topology): stop stable interaction redraw loop 2026-07-23 20:19:53 +08:00
Logic c2d9e08711 fix(instrumentation): focus active stage workspace 2026-07-23 19:41:00 +08:00
Logic 3183197cf7 fix(instrumentation): accept single intake endpoint 2026-07-23 19:30:32 +08:00
Logic 82bcd18416 fix(instrumentation): retain rendered guide stage 2026-07-23 19:16:13 +08:00
Logic ee88746fdf fix(instrumentation): reveal scope by stage 2026-07-23 18:57:59 +08:00
Logic 6a1ffc44ea fix(instrumentation): localize onboarding guidance 2026-07-23 18:41:57 +08:00
Logic 210e9c9e91 fix(instrumentation): preserve advanced query context 2026-07-23 18:24:09 +08:00
Logic 1e0b0818d9 fix(instrumentation): preserve routed service context 2026-07-23 18:06:28 +08:00
Logic f3661382e4 docs(observability): document optional detection context 2026-07-23 18:04:24 +08:00
Logic 022f0648d9 fix(observability): add generic SDK onboarding template 2026-07-23 17:53:49 +08:00
Logic 3186999a63 fix(topology): improve compact graph readability 2026-07-23 17:33:31 +08:00
Logic 047a3c2d12 refactor(topology): split g6 runtime boundaries 2026-07-23 17:20:27 +08:00
Logic 9f8b6668bb fix(topology): stabilize fitted canvas 2026-07-23 16:59:01 +08:00
Logic a4b0c07183 feat(topology): refine graph canvas language 2026-07-23 16:40:39 +08:00
Logic 176f616461 feat(topology): redesign evidence workspace 2026-07-23 16:16:10 +08:00
Logic a5c9512452 fix(topology): add compact selection drawer 2026-07-23 15:12:51 +08:00
Logic 2542cbadf0 fix(topology): render external graph targets 2026-07-23 14:08:11 +08:00
Logic a91a8da595 fix(topology): serialize graph draw updates 2026-07-23 13:58:59 +08:00
Logic 82f70b0b92 chore(frontend): narrow feature exports 2026-07-23 13:45:11 +08:00
Logic 3dfc618742 fix(topology): satisfy exact optional types 2026-07-23 13:37:11 +08:00
Logic 049b9ba967 refactor(entities): split import controller operations 2026-07-23 13:32:12 +08:00
Logic 261833b571 fix(topology): honor frontend architecture boundaries 2026-07-23 13:28:49 +08:00
Logic d5aa78d5af feat(topology): add operator topology page 2026-07-23 13:21:54 +08:00
Logic 0744b2c0ed feat(topology): add G6 canvas runtime 2026-07-23 12:51:06 +08:00
Logic 954e6b926a feat(topology): add page state controller 2026-07-23 12:14:27 +08:00
Logic 9f9981f1eb feat(topology): add graph contract layer 2026-07-23 11:54:51 +08:00
Logic a8abbe9a2a refactor(entities): separate definition editing state 2026-07-23 11:36:00 +08:00
Logic 63fc72e014 feat(entities): add definition editor 2026-07-23 11:28:40 +08:00
Logic d48685876a fix(entities): accept draft definition ids 2026-07-23 10:55:50 +08:00
Logic d699eaec90 fix(entities): harden import preview handling 2026-07-23 10:46:34 +08:00
Logic 2755b86506 feat(entities): add definition import flow 2026-07-23 10:41:29 +08:00
Logic b93447d5be fix(manager): harden entity definition imports 2026-07-23 10:26:36 +08:00
Logic efb2087dcf feat(entities): add resource discovery flow 2026-07-23 09:58:36 +08:00
Logic f33dab7054 perf(manager): batch entity discovery candidates 2026-07-23 09:51:48 +08:00
Logic 7096d98d6b feat(manager): add entity discovery read model 2026-07-23 09:33:54 +08:00
Logic d113aac209 feat(entities): add resource deletion flow 2026-07-23 09:17:44 +08:00
Logic 8d5700ef86 fix(entities): cover derived and related display codes 2026-07-23 09:05:46 +08:00
Logic fd4acad0a0 fix(entities): clarify resource evidence terminology 2026-07-23 09:01:56 +08:00
Logic ece84808fb feat(entities): add resource create and edit flow 2026-07-23 08:48:27 +08:00
Logic 0d3b182f51 fix(entities): simplify resource catalog experience 2026-07-23 08:11:34 +08:00
Logic 0b676d723a feat(entities): add read-only inventory and detail 2026-07-23 07:58:04 +08:00
Logic a1449aa3b9 fix(app): patch Ant Design for React 19 2026-07-23 07:38:13 +08:00
Logic ea9371c87f fix(alerts): accept Spring page metadata 2026-07-23 07:31:11 +08:00
Logic 196fdc398e feat(manager): add secure ui sessions 2026-07-23 07:18:37 +08:00
Logic 3ecf628240 fix(routes): redirect legacy collector path 2026-07-23 06:51:58 +08:00
Logic fee6ffe041 feat(settings): configure plugin parameters 2026-07-23 06:47:08 +08:00
Logic 8801d3ac82 fix(manager): redact parameter validation failures 2026-07-23 06:17:03 +08:00
Logic d0a6386e7a fix(manager): define checkbox parameter wire 2026-07-23 06:01:48 +08:00
Logic a77fbf7f48 feat(alerts): document external integrations 2026-07-23 05:45:43 +08:00
Logic 879cf8bb65 fix(manager): coordinate plugin artifact lifecycle 2026-07-23 05:44:53 +08:00
Logic 3ae6f7f0d4 fix(manager): secure plugin parameter editing 2026-07-23 05:16:11 +08:00
Logic 5b7d0e599b fix(settings): clarify plugin operation state 2026-07-23 05:08:20 +08:00
Logic 9b0bb61f70 feat(settings): manage plugins 2026-07-23 04:54:59 +08:00
Logic a4578662d8 fix(warehouse): select Greptime storage constructor 2026-07-23 04:33:28 +08:00
Logic 3b0d53dede fix(auth): normalize session roles 2026-07-23 04:18:21 +08:00
Logic 9e62a3e303 fix(warehouse): probe Greptime runtime availability 2026-07-23 04:17:57 +08:00
Logic cae6e8fbce fix(observability): complete instrumentation query scope 2026-07-23 03:56:31 +08:00
Logic 7a4c055861 feat(settings): manage monitor definitions 2026-07-23 03:52:35 +08:00
Logic ec80f75ff1 fix(alerter): freeze external alert ingress contract 2026-07-23 03:22:36 +08:00
Logic aea4321cfc fix(auth): stretch lock screen actions 2026-07-23 03:03:11 +08:00
Logic 249ece843f fix(manager): freeze plugin management contract 2026-07-23 03:01:08 +08:00
Logic 1a51d26481 feat(auth): add session lock workflow 2026-07-23 02:46:15 +08:00
Logic c903c9f96c feat(manager): add conditional definition writes 2026-07-23 02:39:05 +08:00
Logic c8a81dd75f fix(manager): keep definition state consistent 2026-07-23 02:06:01 +08:00
Logic 837e69bb2a feat(manager): add monitor definition read contract 2026-07-23 01:58:47 +08:00
Logic ceeb4095c1 feat(collector): add FileLog source editor 2026-07-23 01:52:24 +08:00
Logic f2e2394169 feat(collector): add Prometheus source editor 2026-07-23 01:21:41 +08:00
Logic ad30b78682 refactor(collector): remove unused intake exports 2026-07-23 00:30:59 +08:00
Logic 5452118bc4 feat(collector): add managed runtime core editor 2026-07-23 00:26:13 +08:00
Logic b4d947f7fa fix(collector): restore JVM package launchers 2026-07-23 00:04:51 +08:00
Logic 03a5557988 feat(web): manage collector intake advertisements 2026-07-22 23:41:49 +08:00
Logic bf9799677b test(collector): harden PHP interop boundaries 2026-07-22 23:22:20 +08:00
Logic 5fd9397cad test(collector): prove PHP zero-code interoperability 2026-07-22 23:09:00 +08:00
Logic be18eda567 fix(web): reject stale collector confirmations 2026-07-22 23:03:55 +08:00
Logic 6192aadefd fix(web): harden collector mutation proof 2026-07-22 22:56:24 +08:00
Logic c1730405fb feat(web): add collector management workflow 2026-07-22 22:40:53 +08:00
Logic dda979f9c3 test(collector): prove .NET auto instrumentation 2026-07-22 22:32:37 +08:00
Logic 3deca63ce2 test(collector): prove Go SDK interoperability 2026-07-22 22:20:45 +08:00
Logic bda02c514b fix(web): centralize legacy route redirects 2026-07-22 22:00:39 +08:00
Logic adbae13d88 test(collector): harden Python interop diagnostics 2026-07-22 21:58:31 +08:00
Logic deeb7efa6f fix(web): enforce safe token contracts 2026-07-22 21:48:14 +08:00
Logic 1a1535d8f5 fix(web): parse message server save evidence 2026-07-22 21:36:41 +08:00
Logic b7a09055bf test(collector): prove Python zero-code interoperability 2026-07-22 21:36:21 +08:00
Logic aa6dd269f2 test(collector): prove Node zero-code interoperability 2026-07-22 21:04:53 +08:00
Logic c374cef861 fix(web): isolate dashboard summary states 2026-07-22 20:57:10 +08:00
Logic dc9d641464 fix(web): narrow runtime status domain types 2026-07-22 20:41:15 +08:00
Logic f52a35447d fix(observability): render single-protocol collector targets 2026-07-22 20:38:50 +08:00
Logic f7a65f799a fix(web): correct runtime status evidence 2026-07-22 20:34:29 +08:00
Logic 150e01a2ce feat(manager): aggregate ui runtime status 2026-07-22 20:29:04 +08:00
Logic 47a945fa11 feat(web-app): consume runtime status contracts 2026-07-22 20:19:06 +08:00
Logic f495ee23ce feat(manager): persist collector intake advertisements 2026-07-22 20:15:54 +08:00
Logic 13526a6383 fix(manager): correct runtime status wire contract 2026-07-22 19:57:58 +08:00
Logic e784389c07 feat(manager): freeze collector intake and runtime status contracts 2026-07-22 19:49:40 +08:00
Logic 7290c18cbb fix(web-app): preserve vendor execution order 2026-07-22 19:41:08 +08:00
Logic 7c3803c736 style(web): normalize locale catalogs 2026-07-22 17:12:12 +08:00
Logic 2b90ed6378 refactor(web): own complete refine errors 2026-07-22 14:33:36 +08:00
Logic b02b6e1e61 refactor(web): bind explore history evidence 2026-07-22 14:28:46 +08:00
Logic 289d2e3793 refactor(web): reconstruct receiver write drafts 2026-07-22 14:23:38 +08:00
Logic 9d5c418113 refactor(web): bound alert preview evidence 2026-07-22 14:19:31 +08:00
Logic b45647a568 refactor(web): reconstruct template delete identity 2026-07-22 14:15:18 +08:00
Logic 32a1cacf74 refactor(web): reconstruct receiver delete identity 2026-07-22 14:13:32 +08:00
Logic 57a0876bc3 refactor(web): own instrumentation history stage 2026-07-22 14:10:23 +08:00
Logic f75ef7f10b refactor(web): guard notice receiver draft input 2026-07-22 13:38:05 +08:00
Logic 6ba3b913ef refactor(web): guard notice rule mutation input 2026-07-22 13:36:21 +08:00
Logic 705273060d refactor(web): share mutation ownership guard 2026-07-22 13:34:05 +08:00
Logic 39a844ea35 refactor(web): validate object store draft ownership 2026-07-22 13:31:33 +08:00
Logic 52c84d742c refactor(web): validate notice template input ownership 2026-07-22 13:30:14 +08:00
Logic 73a2cffca5 refactor(web): validate token generation input 2026-07-22 13:28:06 +08:00
Logic e86120d50c refactor(web): harden runtime preferences 2026-07-22 13:25:51 +08:00
Logic 118a4b1433 refactor(web): validate system config inputs 2026-07-22 13:23:13 +08:00
Logic 2cbfd8eb13 refactor(web): allowlist label mutation input 2026-07-22 13:18:32 +08:00
Logic 6fc230ccdb refactor(web): validate shell resource metadata 2026-07-22 13:14:55 +08:00
Logic c17a6c161d refactor(web): bind alert rule query identity 2026-07-22 13:09:52 +08:00
Logic 593bbc1064 refactor(web): separate route manifest ownership 2026-07-22 13:08:08 +08:00
Logic a71fc3bcc0 refactor(web): reconcile monitor selection before commit 2026-07-22 13:04:03 +08:00
Logic f500554174 refactor(web): retire query drafts before commit 2026-07-22 13:03:02 +08:00
Logic 207c7c03e0 refactor(web): validate notice rule evidence 2026-07-22 13:01:34 +08:00
Logic 90f0e1a918 refactor(web): make monitor draft invariants explicit 2026-07-22 12:58:38 +08:00
Logic e764762787 refactor(web): make monitor ids explicit 2026-07-22 12:57:31 +08:00
Logic cfb2e37f72 build(web): enforce chunk boundaries 2026-07-22 12:54:07 +08:00
Logic 54fd1bd674 test(web): enforce refine composition entries 2026-07-22 12:42:52 +08:00
Logic 9ba5b95643 refactor(web): split notice rule entries 2026-07-22 12:40:41 +08:00
Logic 9ce93e54d0 refactor(web): split notice receiver entries 2026-07-22 12:37:33 +08:00
Logic 18890d90bf refactor(web): split object store entries 2026-07-22 12:35:04 +08:00
Logic f03012e351 refactor(web): split system config entries 2026-07-22 12:33:24 +08:00
Logic ef84cacec4 refactor(web): split label composition entries 2026-07-22 12:31:20 +08:00
Logic cfa2f8be6b refactor(web): make bulletin proof explicit 2026-07-22 12:28:02 +08:00
Logic bbc09f5585 refactor(web): make monitor proofs explicit 2026-07-22 12:26:38 +08:00
Logic 28e8a1fe7b refactor(web): share source scoped state 2026-07-22 12:24:08 +08:00
Logic 41b7600f20 fix(web): retire abandoned query drafts 2026-07-22 12:21:20 +08:00
Logic 6fe2872181 refactor(web): narrow status exports 2026-07-22 12:08:41 +08:00
Logic 3d4b19f183 refactor(web): narrow settings exports 2026-07-22 12:05:30 +08:00
Logic 3c284f712b refactor(web): narrow monitor exports 2026-07-22 12:03:15 +08:00
Logic babe371135 refactor(web): narrow instrumentation exports 2026-07-22 12:01:15 +08:00
Logic bcb389bf03 refactor(web): narrow explore and bulletin exports 2026-07-22 11:57:53 +08:00
Logic 68775257fe refactor(web): narrow app and alert exports 2026-07-22 11:55:59 +08:00
Logic d618e64147 refactor(web): share validated page shape 2026-07-22 11:53:16 +08:00
Logic 4042b58393 refactor(web): share compact pagination options 2026-07-22 11:50:22 +08:00
Logic ba74b6777c test(web): consolidate monitor architecture checks 2026-07-22 11:47:34 +08:00
Logic aff1adf9c2 test(web): consolidate status architecture checks 2026-07-22 11:44:33 +08:00
Logic d3e9b99aa9 test(web): verify shell behavior without CSS parsing 2026-07-22 11:42:43 +08:00
Logic 7fca859c6f test(web): remove stale settings navigation scan 2026-07-22 11:41:24 +08:00
Logic 1520583713 test(web): consolidate settings architecture checks 2026-07-22 11:40:20 +08:00
Logic fc80c241f0 test(web): verify theme through public contract 2026-07-22 11:36:29 +08:00
Logic 3c2fbd6f66 test(web): remove duplicate explore source inspection 2026-07-22 11:35:13 +08:00
Logic 6be4cec571 style(web): normalize frontend formatting 2026-07-22 11:32:27 +08:00
Logic 02eecbf15a refactor(web): split explore workbench presentation 2026-07-22 11:30:21 +08:00
Logic 113f2c1d3e test(web): split notice template controller coverage 2026-07-22 11:19:31 +08:00
Logic 1f40a7ab0b refactor(web): organize notice template layers 2026-07-22 11:12:14 +08:00
Logic 6123c580ab refactor(web): organize alert silence layers 2026-07-22 11:05:27 +08:00
Logic d0b49404b3 refactor(web): separate alert rule responsibilities 2026-07-22 10:57:15 +08:00
Logic c57590cd93 refactor(web): organize alert policy layers 2026-07-22 10:46:08 +08:00
Logic 7ee15b7356 test(web): split notice template provider coverage 2026-07-22 10:37:38 +08:00
Logic 044aa57a3d refactor(web): organize alert foundations and provider 2026-07-22 10:31:50 +08:00
Logic 6166958291 refactor(web): move notice rule provider 2026-07-22 10:09:58 +08:00
Logic 5fa5efe50a fix(web): cancel retired notice option reads 2026-07-22 10:02:45 +08:00
Logic e1581af64b refactor(web): move feature data providers 2026-07-22 10:01:23 +08:00
Logic 692a337a84 fix(web): cancel retired status reads 2026-07-22 09:48:04 +08:00
Logic 0a251a4684 fix(web): cancel retired alert reads 2026-07-22 09:46:47 +08:00
Logic d864b4656b fix(web): cancel retired bulletin reads 2026-07-22 09:44:39 +08:00
Logic 1a2c236975 fix(web): remove false instrumentation time ownership 2026-07-22 09:35:55 +08:00
Logic 4a1c65786c refactor(web): centralize alert silence query keys 2026-07-22 09:32:27 +08:00
Logic 56b15007d4 refactor(web): centralize application route paths 2026-07-22 09:30:43 +08:00
Logic d00c1affda build(web): enforce frontend verification toolchain 2026-07-22 09:25:56 +08:00
Logic c8d3add47f fix(web): cancel retired alert inhibit reads 2026-07-22 09:24:20 +08:00
Logic 5dae9980d4 fix(web): make monitor time ownership honest 2026-07-22 09:23:00 +08:00
Logic ad3053a8da refactor(web): move system config provider 2026-07-22 09:14:31 +08:00
Logic 5a11996e00 fix(web): bind alert center evidence 2026-07-22 09:01:53 +08:00
Logic 89119d76ea fix(web): validate status incident pages 2026-07-22 09:00:46 +08:00
Logic 350be5f4d0 fix(web): validate alert silence pages 2026-07-22 08:59:50 +08:00
Logic 357004c3df fix(web): validate alert rule pages 2026-07-22 08:58:22 +08:00
Logic f4737b1be4 fix(web): validate alert inhibit pages 2026-07-22 08:53:21 +08:00
Logic aa737eabd4 fix(web): bind monitor list evidence 2026-07-22 08:51:47 +08:00
Logic 793d9d41f7 fix(web): align bulletin page evidence 2026-07-20 22:31:07 +08:00
Logic 7741a0cc2a fix(web): validate alert group pages 2026-07-20 22:28:11 +08:00
Logic b09ccc9cea fix(web): validate notice rule pages 2026-07-20 22:24:00 +08:00
Logic 14c3152c57 fix(web): bind public status page size 2026-07-20 22:23:15 +08:00
Logic e189614bf6 fix(web): validate collector inventory pages 2026-07-20 22:21:48 +08:00
Logic 4c38c46eeb fix(web): reject duplicate label identities 2026-07-20 22:16:45 +08:00
Logic d37eae4802 fix(web): expose notice rule detail state 2026-07-20 22:15:39 +08:00
Logic a1c9816e19 fix(web): retire stale notice rule drafts 2026-07-20 22:00:05 +08:00
Logic b0cbbb7b76 fix(web): retire stale alert group drafts 2026-07-20 21:58:44 +08:00
Logic ffc3a175d3 refactor(web): validate notice template responses 2026-07-20 21:57:41 +08:00
Logic 78f45d8cd6 fix(web): validate notice receiver pages 2026-07-20 21:46:02 +08:00
Logic d403c9e209 fix(web): validate monitor page snapshots 2026-07-20 21:34:16 +08:00
Logic e1a656547a fix(web): load bounded collector inventory 2026-07-20 21:32:16 +08:00
Logic 3f2b3ea678 fix(web): align explore refresh ownership 2026-07-20 21:26:38 +08:00
Logic cb3ff53bed fix(web): scope bulletin selection to list query 2026-07-20 21:21:32 +08:00
Logic 6b616487ed fix(web): converge label delete pagination 2026-07-20 21:01:45 +08:00
Logic 1f044f5204 fix(web): preserve session failure categories 2026-07-20 20:59:17 +08:00
Logic b4dffcc01f fix(web): map public status evidence honestly 2026-07-20 20:53:05 +08:00
Logic 954330edef fix(web): use causal notice template acknowledgements 2026-07-20 20:35:08 +08:00
Logic d648aff14f fix(web): keep login credentials out of mutation cache 2026-07-20 20:21:19 +08:00
Logic b06239005f fix(web): align shell refresh ownership 2026-07-20 20:18:49 +08:00
Logic 0ddb06893f fix(web): prevent query context key collisions 2026-07-20 19:57:44 +08:00
Logic 93b0ca6500 refactor(web): centralize refine source evidence 2026-07-20 19:43:11 +08:00
Logic 143f697466 fix(web): prioritize status transport evidence 2026-07-20 19:40:21 +08:00
Logic 17d5df8c13 fix(web): prioritize alert group transport evidence 2026-07-20 19:37:42 +08:00
Logic 0b151a6467 fix(web): validate bulletin dependency scans 2026-07-20 19:29:49 +08:00
Logic 5aa9bf9035 fix(web): prioritize receiver transport evidence 2026-07-20 19:29:43 +08:00
Logic 7ff0382d42 fix(web): prioritize template transport evidence 2026-07-20 19:29:38 +08:00
Logic 6ea280e7b4 fix(web): preserve system config uncertainty 2026-07-20 19:21:14 +08:00
Logic b571122e7f fix(web): preserve message server uncertainty 2026-07-20 19:21:08 +08:00
Logic 621386f6d5 fix(web): preserve refine transport causes 2026-07-20 19:15:29 +08:00
Logic d2673e99f7 fix(web): preserve token write uncertainty 2026-07-20 19:13:49 +08:00
Logic ea8fd7b1e8 fix(web): preserve object store write uncertainty 2026-07-20 19:12:21 +08:00
Logic f7d9ae01b8 fix(web): preserve bulletin write uncertainty 2026-07-20 19:04:50 +08:00
Logic e2ca17d916 fix(web): retain uncertain test deliveries 2026-07-20 18:58:23 +08:00
Logic cbba540981 fix(web): preserve label write uncertainty 2026-07-20 18:50:52 +08:00
Logic c8d68eb515 fix(web): preserve silence write uncertainty 2026-07-20 18:50:46 +08:00
Logic dff05919a7 fix(web): preserve receiver write evidence 2026-07-20 18:40:23 +08:00
Logic aeed5fab9f fix(web): centralize inhibit write evidence 2026-07-20 18:40:11 +08:00
Logic d6a2821622 fix(web): retain uncertain notice rule writes 2026-07-20 18:23:57 +08:00
Logic eef720006e fix(web): retain uncertain alert rule saves 2026-07-20 18:23:35 +08:00
Logic 99f5a36061 fix(web): recover uncertain alert group writes 2026-07-20 17:44:18 +08:00
Logic bcc3e3bf2a fix(web): preserve notice template write evidence 2026-07-20 17:44:04 +08:00
Logic 5df6da5704 fix(web): keep status envelopes uncertain 2026-07-20 17:22:01 +08:00
Logic dbc1fc119c fix(web): keep alert group envelopes uncertain 2026-07-20 17:19:52 +08:00
Logic e5c9d750b1 refactor(web): isolate object store write ownership 2026-07-20 17:08:42 +08:00
Logic c4bf1a2dae fix(web): keep token envelope writes uncertain 2026-07-20 16:19:16 +08:00
Logic 400abe628c fix(web): retire stale instrumentation requests 2026-07-20 16:08:03 +08:00
Logic 8de3dce014 fix(web): harden instrumentation response contracts 2026-07-20 15:43:45 +08:00
Logic a0513a041a fix(web): keep message server envelope writes uncertain 2026-07-20 15:27:58 +08:00
Logic 5cda0bfb6c fix(web): keep system config envelope writes uncertain 2026-07-20 15:22:55 +08:00
Logic f64c498729 refactor(web): make bulletin writes recoverable 2026-07-20 15:14:03 +08:00
Logic 7cd02d6b8c refactor(web): recover alert silence writes 2026-07-20 14:59:52 +08:00
Logic c265c29fd1 refactor(web): make label writes recoverable 2026-07-20 14:48:20 +08:00
Logic 453667b87b refactor(web): isolate object store failures 2026-07-20 14:19:28 +08:00
Logic 7c42af7b35 refactor(web): isolate token request failures 2026-07-20 14:01:49 +08:00
Logic dcc58c32ba refactor(web): isolate notice template failures 2026-07-20 13:42:41 +08:00
Logic 9de26bd4d1 test(web): prove object store secret containment 2026-07-20 13:23:36 +08:00
Logic 0c2d9b9df5 refactor(web): isolate notice rule failures 2026-07-20 13:22:11 +08:00
Logic 6045d7b5e8 refactor(web): isolate notice receiver failures 2026-07-20 13:19:59 +08:00
Logic 6486f4dd9b test(web): prove label mutation exclusivity 2026-07-20 13:10:56 +08:00
Logic b4b63124b9 refactor(web): establish auth feature boundary 2026-07-20 13:03:38 +08:00
Logic f887d66b3a refactor(web): isolate alert inhibit transport failures 2026-07-20 12:58:28 +08:00
Logic dbb56b047d refactor(web): move alert rule transport boundary 2026-07-20 12:52:02 +08:00
Logic 2b29a00256 refactor(web): move alert transport boundary 2026-07-20 12:47:06 +08:00
Logic 01e5f60eec refactor(web): clarify login state ownership 2026-07-20 12:44:53 +08:00
Logic ff5d993b43 refactor(web): isolate alert group transport failures 2026-07-20 12:43:27 +08:00
Logic ae02a4236a test(web): enforce inward feature boundaries 2026-07-20 12:34:21 +08:00
Logic eb2de50030 refactor(web): isolate alert rule transport failures 2026-07-20 12:28:28 +08:00
Logic 17bc438b77 refactor(web): model notice receiver state contracts 2026-07-20 12:21:23 +08:00
Logic d9d681ed98 refactor(web): isolate alert center transport failures 2026-07-20 12:19:03 +08:00
Logic df9d71ccf8 fix(web): gate monitor metric refresh context 2026-07-20 12:11:32 +08:00
Logic d975ce905c refactor(web): isolate dashboard transport failures 2026-07-20 12:09:24 +08:00
Logic f0502a0bfb refactor(web): move notice template state to model 2026-07-20 12:06:12 +08:00
Logic 240612f9a3 refactor(web): model bulletin dependency proof 2026-07-20 12:05:18 +08:00
Logic 884efe0ad9 refactor(web): isolate status transport failures 2026-07-20 11:59:46 +08:00
Logic c55703a988 style(web): format alert rule model 2026-07-20 11:58:10 +08:00
Logic 6486b480ff refactor(web): move alert rule list state to model 2026-07-20 11:49:20 +08:00
Logic 7768cc8550 refactor(web): decouple instrumentation presentation types 2026-07-20 11:47:21 +08:00
Logic a21be5add8 refactor(web): move alert inhibit state contracts 2026-07-20 11:38:44 +08:00
Logic cbf5b83126 test(web): enforce inward feature dependencies 2026-07-20 11:34:41 +08:00
Logic 0b463856d3 test(web): replace provider raw assertions 2026-07-20 11:33:08 +08:00
Logic ddcca11aff fix(web): preserve silence audit timestamps 2026-07-20 11:24:18 +08:00
Logic 28c41fdc0a test(web): replace bulletin raw assertions 2026-07-20 11:18:52 +08:00
Logic 31ef4efa06 test(web): remove explore page raw assertions 2026-07-20 11:16:35 +08:00
Logic 1b3e551428 test(web): guard notification presentation boundaries 2026-07-20 11:14:18 +08:00
Logic 82676b4cbb test(web): prove explore query key interoperability 2026-07-20 11:11:49 +08:00
Logic 7a767314b7 test(web): enforce feature runtime boundaries 2026-07-20 11:03:03 +08:00
Logic 900af42e10 test(web): remove alert rule raw assertions 2026-07-20 11:00:53 +08:00
Logic f6a5e4a232 test(web): enforce query key factory usage 2026-07-20 10:59:08 +08:00
Logic 0cfdfe02e6 test(web): replace token raw architecture assertions 2026-07-20 10:48:30 +08:00
Logic 7f8a99c75e fix(web): prove message server writes 2026-07-20 10:46:48 +08:00
Logic 9d2cb34c9b test(web): replace alert raw architecture assertions 2026-07-20 10:44:31 +08:00
Logic 8e09691846 test(web): replace raw ownership assertions 2026-07-20 10:35:44 +08:00
Logic 4024145a3c refactor(web): name alert rule operation state 2026-07-20 10:33:10 +08:00
Logic ab0829a72f refactor(web): name notice reread status mapping 2026-07-20 10:33:02 +08:00
Logic effb7b41f6 refactor(web): move alert group paths to API 2026-07-20 10:24:09 +08:00
Logic b7b774bc4e refactor(web): clarify instrumentation progress states 2026-07-20 10:21:53 +08:00
Logic 188bd9908f fix(web): scope live log state by stream 2026-07-20 10:21:43 +08:00
Logic ec79afe9c0 refactor(web): clarify shell navigation state 2026-07-20 10:21:36 +08:00
Logic ba0237d36b fix(web): unify alert summary evidence 2026-07-20 10:19:20 +08:00
Logic e033b263a5 refactor(web): centralize notice endpoints 2026-07-20 10:16:24 +08:00
Logic 875724a9f7 refactor(web): split alert silence schedule model 2026-07-20 10:07:08 +08:00
Logic a5ed0de28f fix(web): keep bulletin evidence unverified 2026-07-20 10:00:43 +08:00
Logic 77499f688f fix(web): prove system config writes 2026-07-20 09:55:25 +08:00
Logic 38ffceef2e refactor(web): centralize object store endpoint 2026-07-20 09:50:47 +08:00
Logic df3e9dd4ec refactor(web): centralize alert silence endpoint 2026-07-20 09:46:41 +08:00
Logic 796b50b92c refactor(web): own status editor defaults 2026-07-20 09:42:14 +08:00
Logic eff89ed3ac fix(web): prove object store writes 2026-07-20 09:38:46 +08:00
Logic 0dbeff7bd5 refactor(web): centralize label endpoint 2026-07-20 09:34:02 +08:00
Logic b7b4957fd9 refactor(web): centralize remaining route paths 2026-07-20 09:26:47 +08:00
Logic f95790603c refactor(web): centralize timezone endpoint 2026-07-20 09:16:29 +08:00
Logic c3004ac300 fix(web): retain uncertain token writes 2026-07-20 09:14:17 +08:00
Logic 25f1d4d28c refactor(web): name request url resolution 2026-07-20 09:11:37 +08:00
Logic 0c5bb6f1ef fix(web): keep missing trace timing explicit 2026-07-20 09:09:13 +08:00
Logic 5aff82931b refactor(web): centralize alert route paths 2026-07-20 09:08:28 +08:00
Logic c2eed20dd2 refactor(web): move write rejection to api boundary 2026-07-20 08:57:27 +08:00
Logic 8df2a2b3d3 refactor(web): restore label dependency direction 2026-07-20 08:56:23 +08:00
Logic f405d46271 test(web): make architecture checks style independent 2026-07-20 08:54:44 +08:00
Logic 2a2d4add0b fix(web): restore collapsed navigation 2026-07-20 08:47:48 +08:00
Logic 6b425161fe fix(web): assign one header refresh owner 2026-07-20 08:45:09 +08:00
Logic bb71323c4d refactor(web): centralize application routes 2026-07-20 08:40:13 +08:00
Logic 6759a62ac6 fix(web): serialize locale publication 2026-07-20 08:25:01 +08:00
Logic 32ca74623a fix(web): converge expired session identity 2026-07-20 08:21:32 +08:00
Logic 860918adfe fix(web): redact route error diagnostics 2026-07-20 08:18:56 +08:00
Logic 4c9e4b12d1 fix(web): renew event stream recovery episodes 2026-07-20 08:12:05 +08:00
Logic a26f143ad3 fix(web): prove alert inhibit writes 2026-07-20 08:11:02 +08:00
Logic cd8bc03a29 fix(web): prove status management writes 2026-07-20 08:04:38 +08:00
Logic 6c8b1d6345 fix(web): preserve message server write proof 2026-07-20 07:51:19 +08:00
Logic 61b1025105 fix(web): prove notice receiver writes 2026-07-20 07:35:29 +08:00
Logic 037208cc8c fix(web): prove notice template writes 2026-07-20 07:19:39 +08:00
Logic 0d7b843d5d fix(web): recover alert rule list commands 2026-07-20 06:54:11 +08:00
Logic b1810e40c7 fix(web): preserve notice template command ownership 2026-07-20 06:34:58 +08:00
Logic 314603c64c refactor(web): constrain refine provider records 2026-07-20 06:19:21 +08:00
Logic e0dfd4d76c fix(web): serialize label mutations 2026-07-20 06:16:51 +08:00
Logic b5470871b8 fix(web): harden alert group commands 2026-07-20 06:14:06 +08:00
Logic 9f2fce51ed fix(web): lock alert silence modal close 2026-07-20 06:12:56 +08:00
Logic 63a23a7706 fix(web): serialize alert silence workflows 2026-07-20 06:10:30 +08:00
Logic 8289b7537b fix(web): isolate settings write ownership 2026-07-20 05:51:32 +08:00
Logic 64a6e1a0d8 refactor(web): finish bulletin size cleanup 2026-07-20 05:45:27 +08:00
Logic ccf16bc392 refactor(web): isolate shell header actions 2026-07-20 05:43:07 +08:00
Logic 09481a46d1 fix(web): serialize bulletin editor workflows 2026-07-20 05:40:02 +08:00
Logic 9e59a0d5ef fix(web): require complete public status incidents 2026-07-20 05:38:03 +08:00
Logic 5cb81c7d58 refactor(web): isolate browser event stream lifecycle 2026-07-20 05:36:40 +08:00
Logic 325bea9cda fix(web): serialize token secret commands 2026-07-20 05:36:04 +08:00
Logic 382434b1dc fix(web): unify explore filter contracts 2026-07-20 05:22:03 +08:00
Logic b21b682d4b refactor(web): centralize typed provider boundaries 2026-07-20 05:05:43 +08:00
Logic c504e3ed4f refactor(web): harden monitor workflow boundaries 2026-07-20 04:47:41 +08:00
Logic e9c4beac08 fix(web): retire stale alert inhibit work 2026-07-20 02:45:36 +08:00
Logic 1944a21f4d fix(web): serialize notice template commands 2026-07-20 02:27:19 +08:00
Logic 702943d450 fix(web): serialize alert rule editor work 2026-07-20 02:09:39 +08:00
Logic b2e082c35f fix(web): make notice receivers transactional 2026-07-20 01:53:39 +08:00
Logic d7e72ee668 fix(web): make alert inhibits transactional 2026-07-20 00:05:38 +08:00
Logic 88299939c5 fix(web): harden notice rule workflows 2026-07-19 23:27:31 +08:00
Logic 843f1b9996 fix(web): harden alert group commands 2026-07-19 22:36:21 +08:00
Logic e7b1180e8a fix(web): secure instrumentation boundaries 2026-07-19 21:58:36 +08:00
Logic ccdbcc68f3 fix(web): prove message server saves 2026-07-19 21:07:45 +08:00
Logic 27054df1e7 fix(web): clear stale trace selections 2026-07-19 20:44:42 +08:00
Logic 9f6bc50ee6 refactor(web): unify metric result state 2026-07-19 20:31:32 +08:00
Logic 3904a6961d refactor(web): isolate email server editor 2026-07-19 20:17:48 +08:00
Logic b0e9c89d69 fix(web): keep explore preset windows relative 2026-07-19 20:02:22 +08:00
Logic 536b6017e4 refactor(web): isolate alert silence operation gate 2026-07-19 19:55:00 +08:00
Logic 5858a014a2 refactor(web): isolate label mutation parameters 2026-07-19 19:46:03 +08:00
Logic 64503f2eff refactor(web): split instrumentation guide presentation 2026-07-19 19:39:26 +08:00
Logic 6b86c37621 refactor(web): split status organization presentation 2026-07-19 19:28:37 +08:00
Logic 1058b8ac92 refactor(web): split status incident presentation 2026-07-19 19:20:54 +08:00
Logic e1644b9fd8 refactor(web): split notice rule advanced fields 2026-07-19 19:09:51 +08:00
Logic 83353aa7a3 refactor(web): split alert silence schedule fields 2026-07-19 19:00:07 +08:00
Logic 74fbb29bfa refactor(web): extract status management header 2026-07-19 18:51:41 +08:00
Logic 5b2a22b9d4 refactor(web): restore public status ownership 2026-07-19 18:41:16 +08:00
Logic ba1b79ef76 refactor(web): split label result columns 2026-07-19 18:30:26 +08:00
Logic f54ba04fdf refactor(web): split alert center toolbar fields 2026-07-19 18:22:11 +08:00
Logic 0e8bc6d3a0 refactor(web): own system config contracts in model 2026-07-19 18:13:38 +08:00
Logic e0bfa02f6a refactor(web): move status incident query into controller 2026-07-19 18:06:20 +08:00
Logic d855a36476 refactor(web): move status incident editor into controller 2026-07-19 17:57:44 +08:00
Logic 8a9458b558 refactor(web): move monitor selection into controller 2026-07-19 17:48:04 +08:00
Logic d88a36a2d8 refactor(web): move Explore submission into controller 2026-07-19 17:39:25 +08:00
Logic 93bc46c34d build(web): enforce maintainability debt boundaries 2026-07-19 17:30:03 +08:00
Logic fbb41c3665 fix(web): send monitor enable as mutation 2026-07-19 17:08:15 +08:00
Logic 85d1f62398 style(web): format monitor contract coverage 2026-07-19 17:05:04 +08:00
Logic 202e9f0f0a fix(web): sanitize authentication redirects 2026-07-19 16:56:55 +08:00
Logic 7fcb212b6c fix(web): isolate caches across browser identities 2026-07-19 16:44:36 +08:00
Logic 80ea537c50 fix(web): restore frontend tooling gates 2026-07-19 16:17:23 +08:00
Logic 334d5d238d fix(web): clear stale Explore evidence 2026-07-19 12:23:14 +08:00
Logic eee535baef refactor(web): split Explore presentation modules 2026-07-19 12:17:17 +08:00
Logic 44c49064a5 fix(web): keep Explore preset windows rolling 2026-07-19 12:11:19 +08:00
Logic ca9d8c4613 fix(web): distinguish missing metric context 2026-07-19 11:19:30 +08:00
Logic c475944c29 fix(web): format staged worktree paths 2026-07-19 11:14:49 +08:00
Logic 3540892045 refactor(web): normalize Explore component formatting 2026-07-19 10:57:45 +08:00
Logic ccdafcdae7 build(web): format staged frontend changes 2026-07-19 10:52:32 +08:00
Logic ddf4b5a38e fix(web): clarify fixed Explore windows 2026-07-19 10:38:34 +08:00
Logic e74666407e fix(web): isolate Explore signal queries 2026-07-19 10:27:41 +08:00
Logic 8cba67130f test(web): stabilize monitor URL convergence 2026-07-19 03:58:53 +08:00
Logic 953f32bdfc fix(web): neutralize elevated surfaces 2026-07-19 03:09:50 +08:00
Logic 8be9edfc9e fix(web): restore bulletin monitor filtering 2026-07-19 02:55:23 +08:00
Logic 24c5864d6d refactor(web): stabilize notice template workspace 2026-07-19 02:18:18 +08:00
Logic 78fec7dc58 fix(web): time out stalled API requests 2026-07-19 01:58:29 +08:00
Logic 11039c06b8 fix(web): fit notice template results to workspace 2026-07-19 01:29:49 +08:00
Logic 17effd594f refactor(web): keep settings navigation in shell 2026-07-19 01:12:41 +08:00
Logic 447acb3bf3 refactor(web): validate notice receiver provider inputs 2026-07-19 00:19:32 +08:00
Logic 49f9cb6800 refactor(web): validate notice template provider inputs 2026-07-19 00:00:12 +08:00
Logic 2f764c4876 fix(web): synchronize alert filter drafts after navigation 2026-07-18 23:43:57 +08:00
Logic 7959c30405 refactor(web): validate alert silence provider inputs 2026-07-18 23:23:07 +08:00
Logic 26245ef789 fix(web): restore active navigation background 2026-07-18 23:07:37 +08:00
Logic 2032a29f75 fix(web): simplify active navigation indicator 2026-07-18 22:53:36 +08:00
Logic 88fcd434c4 refactor(web): split notice template controller 2026-07-18 22:46:57 +08:00
Logic f8b58388d6 refactor(web): share URL query draft state 2026-07-18 22:20:15 +08:00
Logic 444182faec refactor(web): validate API message boundaries 2026-07-18 22:11:18 +08:00
Logic 4eae3eb43d refactor(web): define session runtime schema 2026-07-18 21:53:11 +08:00
Logic b6ff85dffa fix(web): enable Ant theme variables 2026-07-18 21:40:59 +08:00
Logic bd44661b8a refactor(web): use semantic explore colors 2026-07-18 20:52:19 +08:00
Logic adc687767b refactor(web): use semantic login surfaces 2026-07-18 20:43:52 +08:00
Logic 4af11648ee refactor(web): split trace result presentation 2026-07-18 20:37:57 +08:00
Logic d73d5e12fd refactor(web): split log result presentation 2026-07-18 20:28:25 +08:00
Logic b70bb2e210 refactor(web): split token page sections 2026-07-18 20:15:17 +08:00
Logic 26b7a1b993 refactor(web): centralize trace detail query key 2026-07-18 20:05:37 +08:00
Logic adbc203cac refactor(web): define explore history query identity 2026-07-18 19:59:29 +08:00
Logic e61f3bb268 refactor(web): centralize public status query keys 2026-07-18 19:45:18 +08:00
Logic 0855fea703 refactor(web): centralize message server query keys 2026-07-18 19:39:17 +08:00
Logic b5bb909605 refactor(web): name dashboard query key 2026-07-18 19:33:57 +08:00
Logic 5f6f5ecc5a refactor(web): modularize notice templates 2026-07-18 19:30:18 +08:00
Logic 949e36833c refactor(web): split alert group controller 2026-07-18 19:06:11 +08:00
Logic 0e5d690d4b refactor(web): centralize alert rule query keys 2026-07-18 18:56:05 +08:00
Logic 0a96c0f0ac refactor(web): separate notice rule reads 2026-07-18 18:46:42 +08:00
Logic a747c9afea refactor(web): modularize alert center 2026-07-18 18:32:43 +08:00
Logic 9bdc863d01 refactor(web): remove unused public exports 2026-07-18 18:08:57 +08:00
Logic c059f7ef3d refactor(web): share named remote payloads 2026-07-18 18:05:15 +08:00
Logic 9cd4007532 refactor(web): share strict remote states 2026-07-18 17:41:31 +08:00
Logic 51ee1d2a57 feat(web): restore bulletin metric tree 2026-07-18 16:23:32 +08:00
Logic 1172e84d75 feat(web): add bulletin metric tree contract 2026-07-18 15:55:10 +08:00
Logic 346bff9e46 refactor(web): restore bulletin controller boundaries 2026-07-18 15:37:15 +08:00
Logic 9243fc606f refactor(web): restore token feature boundaries 2026-07-18 15:14:27 +08:00
Logic 3f71ccf74d refactor(web): split status management controller 2026-07-18 14:45:32 +08:00
Logic ebf51eab16 refactor(web): restore status wire schemas 2026-07-18 14:34:30 +08:00
Logic 193e69018c test(web): align architecture and silence fixtures 2026-07-18 14:00:57 +08:00
Logic 6016d987e0 fix(web): bound live log reconnects 2026-07-18 13:52:44 +08:00
Logic c4238774a1 refactor(web): restore explore wire schemas 2026-07-18 13:45:05 +08:00
Logic 0d79dc0b52 refactor(web): make detection state exhaustive 2026-07-18 13:33:30 +08:00
Logic 338f2ca719 refactor(web): split monitor editor domain model 2026-07-18 13:23:14 +08:00
Logic 3687382a65 fix(web): expose monitor refresh failures 2026-07-18 13:02:19 +08:00
Logic 45ef796c26 refactor(web): centralize monitor query keys 2026-07-18 12:55:05 +08:00
Logic f8325dc4bb refactor(web): replace monitor primitive parsers 2026-07-18 12:45:44 +08:00
Logic 9bf13d5e58 fix(web): satisfy architecture script lint 2026-07-18 12:45:08 +08:00
Logic 245d34fbc0 build(web): enforce feature architecture debt ceilings 2026-07-18 12:29:54 +08:00
Logic 657a1d07c5 feat(web): add explore instance context filters 2026-07-18 12:20:38 +08:00
Logic ca8da85868 refactor(web): split monitor editor controller 2026-07-18 11:58:40 +08:00
Logic c49c0fa43c refactor(web): isolate monitor read schemas 2026-07-18 11:34:17 +08:00
Logic 75443bbeb5 fix(web): bound label canonical reads 2026-07-18 11:23:52 +08:00
Logic e89013b9ad fix(web): keep object store secrets out of read state 2026-07-18 11:12:42 +08:00
Logic 1581c6c81c refactor(web): model alert silence detail states 2026-07-18 10:57:27 +08:00
Logic 9985c029ca fix(web): bound alert rule creation proof 2026-07-18 10:46:36 +08:00
Logic 32132fb3d1 fix(web): preserve alert silence write evidence 2026-07-18 10:40:00 +08:00
Logic 5e8ab73821 refactor(web): isolate alert rule response schemas 2026-07-18 10:21:48 +08:00
Logic dba9ee3541 refactor(web): isolate alert center response schemas 2026-07-18 10:13:57 +08:00
Logic 6bd669953f refactor(web): isolate alert silence response schemas 2026-07-18 10:06:40 +08:00
Logic 65261995bd fix(web): classify missing alert silence details 2026-07-18 10:05:58 +08:00
Logic 89285894b2 refactor(web): isolate alert inhibit response schemas 2026-07-18 09:57:09 +08:00
Logic f156b70ad9 docs(web): explain contract boundary invariants 2026-07-18 09:41:40 +08:00
Logic f03e121f58 refactor(web): isolate alert group response schemas 2026-07-18 00:31:07 +08:00
Logic ea67d5caec refactor(web): isolate dashboard response schemas 2026-07-18 00:24:35 +08:00
Logic a28ce07c74 refactor(web): validate message server contracts with schemas 2026-07-18 00:20:07 +08:00
Logic 00711dd018 refactor(web): validate notice receiver contracts with schemas 2026-07-18 00:15:28 +08:00
Logic 273dd27c01 refactor(web): validate bulletin contracts with schemas 2026-07-18 00:09:38 +08:00
Logic 5739bf6aa2 docs(web): codify evidence-first agent workflow 2026-07-17 23:46:59 +08:00
Logic 073a3be8f5 test(web): stabilize the full frontend suite 2026-07-17 23:46:51 +08:00
Logic 183d0862c1 test(web): keep node architecture fixtures out of vitest 2026-07-17 23:33:37 +08:00
Logic 5ffd851350 refactor(web): establish instrumentation controller boundary 2026-07-17 23:26:32 +08:00
Logic 57c4a9baa1 refactor(web): validate instrumentation contracts with schemas 2026-07-17 23:09:20 +08:00
Logic 6277de5b3f refactor(web): enforce React architecture boundaries 2026-07-17 23:01:21 +08:00
Logic be3f85a445 feat(web): virtualize log investigation results 2026-07-17 21:42:16 +08:00
Logic 03b9c88823 feat(observability): scope queries by instance and route 2026-07-17 21:26:33 +08:00
Logic 92b9a00d8c feat(web): add trace investigation workspace 2026-07-17 21:17:59 +08:00
Logic 208de7f9ab fix(web): preserve Explore result states 2026-07-17 20:50:59 +08:00
Logic b21951b632 feat(web): complete instrumentation onboarding 2026-07-17 20:35:39 +08:00
Logic ca986c0fa7 feat(web): unify query and time context 2026-07-17 19:51:15 +08:00
Logic 7f7b52b9ec feat(web): establish Horizon operator shell 2026-07-17 19:14:00 +08:00
Logic 2eedb8e905 feat(collector): add systemd release lifecycle 2026-07-17 19:05:53 +08:00
Logic 676f0f4773 build(collector): verify native archive before extraction 2026-07-17 18:49:09 +08:00
Logic 41fa9e43de test(collector): run native image release gate 2026-07-17 18:38:29 +08:00
Logic 4a7861827d feat(web): complete Bulletin workflow 2026-07-17 18:33:56 +08:00
Logic 16c3bcf551 fix(collector): qualify release SDK scanning 2026-07-17 18:24:41 +08:00
Logic 6458ca90dc test(observability): freeze instrumentation v1 surface 2026-07-17 18:19:26 +08:00
Logic 2d964873ff test(remoting): prove manager reconnect lifecycle 2026-07-17 16:44:30 +08:00
Logic d7ab3f7818 test(collector): prove packaged active collection 2026-07-17 16:33:49 +08:00
Logic 2f8e3661d6 feat(web): harden notice rule workflow 2026-07-17 16:01:33 +08:00
Logic e9c7b432df test(observability): verify three-signal query handoff 2026-07-17 15:58:05 +08:00
Logic 0a9c303769 test(observability): prove scoped Greptime signal detection 2026-07-17 15:48:03 +08:00
Logic f64e30f319 feat(web): secure notice receiver resource 2026-07-17 15:20:46 +08:00
Logic 23bc24ac68 fix(collector): detect ecosystem SDK packages 2026-07-17 15:20:25 +08:00
Logic 04b2671a96 build(collector): reject bundled language agents 2026-07-17 15:14:14 +08:00
Logic 4ba4a6dbcd feat(observability): scope signal queries by collector 2026-07-17 14:57:08 +08:00
Logic e21d67936c test(collector): verify packaged native runtime lifecycle 2026-07-17 14:38:34 +08:00
Logic fd2095efde feat(web): harden message server configuration 2026-07-17 14:29:02 +08:00
Logic 07b89a7806 fix(collector): register managed runtime native hints 2026-07-17 14:10:03 +08:00
Logic 97ba3fa280 fix(alerter): secure alert silence contracts 2026-07-17 14:05:57 +08:00
Logic 1b6f1b57c0 fix(web): harden login session workflow 2026-07-17 13:55:44 +08:00
Logic 24da58c4b8 fix(manager): secure message server configs 2026-07-17 13:52:45 +08:00
Logic adee55be54 feat(web): complete monitor editor workflow 2026-07-17 13:46:46 +08:00
Logic 2298e37201 fix(alerter): secure notice receiver contracts 2026-07-17 13:18:55 +08:00
Logic a4e2963832 fix(manager): secure auth token contracts 2026-07-17 12:54:05 +08:00
Logic b63808f316 fix(manager): validate monitor schedules 2026-07-17 12:43:23 +08:00
Logic 4f6b5cf683 fix(manager): canonicalize monitor instances 2026-07-17 12:33:49 +08:00
Logic 4aad504ea8 refactor(web): isolate monitor metric workbench 2026-07-17 07:29:45 +08:00
Logic c8abd12da0 refactor(web): isolate monitor detail controller 2026-07-17 06:59:01 +08:00
Logic fdd41f0244 fix(web): harden Alert Silence controller boundaries 2026-07-17 06:34:19 +08:00
Logic 24f43febc8 refactor(web): isolate Alert Silence controller 2026-07-17 06:22:36 +08:00
Logic 893e047005 refactor(web): isolate monitor list controller 2026-07-17 06:03:24 +08:00
Logic 8024701184 refactor(web): isolate dashboard data flow 2026-07-17 05:40:39 +08:00
Logic b556d2e563 refactor(web): route instrumentation query handoffs 2026-07-17 05:28:50 +08:00
Logic 61a877c95f fix(web): converge instrumentation contract refresh 2026-07-17 05:23:00 +08:00
Logic e8fd8b26e3 refactor(web): isolate trace detail controller 2026-07-17 05:14:13 +08:00
Logic 8eb1131d14 refactor(web): isolate live log controller 2026-07-17 05:04:33 +08:00
Logic 48e55a0d5f refactor(web): isolate Explore page controller 2026-07-17 04:48:51 +08:00
Logic da1726ac08 refactor(web): enforce Explore signal contracts 2026-07-17 04:31:59 +08:00
Logic 9a72abb672 refactor(web): isolate Alert Rule editor controller 2026-07-17 04:10:01 +08:00
Logic 8d43883d1e refactor(web): isolate Alert Rule list controller 2026-07-17 03:47:56 +08:00
Logic db98bc618e refactor(web): enforce Alert Rule contracts 2026-07-17 03:40:42 +08:00
Logic a7b76b6524 refactor(web): isolate Alert Inhibit controller 2026-07-17 03:24:11 +08:00
Logic 520f303374 refactor(web): enforce Alert Inhibit contracts 2026-07-17 03:13:38 +08:00
Logic 70b5ac1d6b refactor(web): isolate Alert Group controller 2026-07-17 03:07:57 +08:00
Logic c6384dc8b1 refactor(web): enforce Alert Group contracts 2026-07-17 02:54:18 +08:00
Logic f28050a5f5 refactor(web): isolate Alert Center controller 2026-07-17 02:45:53 +08:00
Logic 774f974a71 feat(web): add honest alert state copy 2026-07-17 02:27:28 +08:00
Logic 40f913217c refactor(web): enforce Alert Center contracts 2026-07-17 02:17:56 +08:00
Logic 7fa215f38f feat(web): add Alert Silence Refine provider 2026-07-17 02:04:36 +08:00
Logic 4bfa99541f refactor(web): enforce Status Management transactions 2026-07-17 01:42:13 +08:00
Logic c485413b65 refactor(web): enforce Status Management contracts 2026-07-17 01:17:31 +08:00
Logic 6cbb6c531b refactor(web): migrate Notice Template to Refine 2026-07-17 00:50:13 +08:00
Logic 65694a7c39 refactor(web): migrate Token management to Refine 2026-07-16 23:20:21 +08:00
Logic 57bb51be2a fix(web): distinguish Object Store contract errors 2026-07-16 22:52:23 +08:00
Logic 61579f28f7 refactor(web): migrate System Config to Refine 2026-07-16 22:50:22 +08:00
Logic 195101795c refactor(web): migrate Object Store to Refine 2026-07-16 22:35:26 +08:00
Logic 0d8976ae8a feat(web): consume Collector intake discovery 2026-07-16 21:52:51 +08:00
Logic 5144f859ee fix(web): keep instrumentation states honest 2026-07-16 21:40:59 +08:00
Logic 9d2c4a23e0 fix(web): restore brand and navigation states 2026-07-16 21:32:07 +08:00
Logic a56432816d refactor(web): establish instrumentation controllers 2026-07-16 21:12:18 +08:00
Logic d4b14d2800 refactor(web): align instrumentation v1 contract 2026-07-16 20:49:59 +08:00
Logic 6362213063 refactor(web): migrate Label resource to Refine 2026-07-16 20:17:38 +08:00
Logic d791e54421 refactor(web): isolate Label URL query state 2026-07-16 19:58:26 +08:00
Logic 7864b7b86d feat(web): add Label Refine data provider 2026-07-16 19:48:31 +08:00
Logic 4d42ef00ac feat(web): establish Refine runtime ownership 2026-07-16 19:28:58 +08:00
Logic 1df1a8cc54 refactor(web): normalize route module casing 2026-07-16 19:18:45 +08:00
Logic 952691f572 fix(observability): render Windows instrumentation commands 2026-07-16 19:14:33 +08:00
Logic 9ef469dd4d build(web): gate official Refine AntD5 stack 2026-07-16 19:12:35 +08:00
Logic cb6727d181 feat(observability): bind guide secret placeholders 2026-07-16 19:07:45 +08:00
Logic 5870a6792e test(observability): freeze instrumentation provenance 2026-07-16 19:01:32 +08:00
Logic 569a188c9a feat(observability): diagnose instrumentation readiness 2026-07-16 18:50:18 +08:00
Logic bb513f6b27 refactor(web): type explore query submissions 2026-07-16 17:12:45 +08:00
Logic 6ef70eeea0 refactor(web): isolate status incident query state 2026-07-16 16:35:37 +08:00
Logic d7959041c8 fix(web): preserve status organization drafts 2026-07-16 16:25:10 +08:00
Logic 3305f93e32 fix(web): prevent stale incident detail updates 2026-07-16 16:09:46 +08:00
Logic cfca0ac1da fix(web): scope monitor bulk selection 2026-07-16 15:56:39 +08:00
Logic 075c7f7e41 fix(web): preserve honest metric result states 2026-07-16 15:23:52 +08:00
Logic 10e0d20a99 fix(web): preserve unknown trace states 2026-07-16 15:12:45 +08:00
Logic b7df9d6bb4 fix(web): fail closed on invalid explore scope 2026-07-16 15:02:48 +08:00
Logic b261385e63 refactor(web): model explore submissions 2026-07-16 14:47:35 +08:00
Logic cadbd66684 refactor(web): normalize alert module paths 2026-07-16 14:19:50 +08:00
Logic 9e94a01ec0 feat(web): add status management workflow 2026-07-16 14:15:57 +08:00
Logic 2be7c520d7 feat(web): add status management contract 2026-07-16 14:04:55 +08:00
Logic 843ae657b9 refactor(web): layer public status 2026-07-16 14:00:19 +08:00
Logic 6200028366 fix(web): preserve public status errors 2026-07-16 13:55:18 +08:00
Logic a7bb544e5c refactor(web): normalize public status paths 2026-07-16 13:53:29 +08:00
Logic 0b427bb80b fix(web): classify public status failures 2026-07-16 13:51:22 +08:00
Logic 06ff400704 refactor(web): preserve API message errors 2026-07-16 13:48:56 +08:00
Logic 922627a397 refactor(web): layer object store settings 2026-07-16 13:43:46 +08:00
Logic 5cb17992e8 refactor(web): layer label settings 2026-07-16 13:38:37 +08:00
Logic 36a6489db0 refactor(web): layer system configuration settings 2026-07-16 13:30:00 +08:00
Logic 3a6a87c71e refactor(web): layer token settings 2026-07-16 13:25:33 +08:00
Logic c1aa724fa7 refactor(web): layer message server settings 2026-07-16 13:22:16 +08:00
Logic 97125aa846 feat(web): add object store settings 2026-07-16 13:16:17 +08:00
Logic f4d07e15ea feat(web): add label settings 2026-07-16 13:11:55 +08:00
Logic 59a4212cac feat(web): add system configuration settings 2026-07-16 13:08:21 +08:00
Logic d1dcf88741 refactor(web): normalize message server module paths 2026-07-16 13:03:36 +08:00
Logic 66472ce192 refactor(web): layer monitor feature 2026-07-16 12:59:38 +08:00
Logic b6f69ecfff feat(web): add monitor label filter 2026-07-16 12:53:47 +08:00
Logic c73e1972af refactor(web): normalize monitor module paths 2026-07-16 12:48:23 +08:00
Logic 84dda3d130 feat(web): preserve monitor label query context 2026-07-16 12:45:34 +08:00
Logic 6a23056cfc refactor(web): normalize locale asset paths 2026-07-16 12:41:26 +08:00
Logic 601ecff43c refactor(web): layer explore feature 2026-07-16 12:34:41 +08:00
Logic 395c0a5fcd refactor(web): layer instrumentation feature 2026-07-16 12:25:01 +08:00
Logic d6e6f5f8ab test(collector): add runtime capacity soak gate 2026-07-16 01:42:19 +08:00
Logic 63b88d2393 test(collector): add runtime fault load gate 2026-07-16 01:17:16 +08:00
Logic 0a85b90f7c feat(collector): bound runtime backpressure 2026-07-16 00:55:26 +08:00
Logic 47e7ecbd81 feat(web): preserve instrumentation query context 2026-07-16 00:49:00 +08:00
Logic 64ac21d8c2 feat(web): add instrumentation onboarding runbook 2026-07-16 00:32:51 +08:00
Logic bb1fb40112 fix(collector): converge runtime failure recovery 2026-07-16 00:28:43 +08:00
Logic 17a0c24845 feat(collector): report managed runtime telemetry 2026-07-16 00:09:10 +08:00
Logic 305a62fa09 fix(observability): align instrumentation onboarding guides 2026-07-15 23:46:22 +08:00
Logic c7032f7557 test(collector): verify official Java agent interoperability 2026-07-15 23:29:21 +08:00
Logic 656ce3001b fix(observability): wire instrumentation detection at startup 2026-07-15 23:14:44 +08:00
Logic 77e9404f19 feat(observability): detect scoped signals in Greptime 2026-07-15 23:05:29 +08:00
Logic 91b4cc8fa8 feat(web): add instrumentation onboarding contract client 2026-07-15 23:03:53 +08:00
Logic ec065cdeee feat(observability): freeze instrumentation onboarding API 2026-07-15 22:47:09 +08:00
Logic a4ecce3425 feat(web): add API token management workflow 2026-07-15 22:16:43 +08:00
Logic 7a9e006d6a feat(web): move notification configuration to settings 2026-07-15 22:09:52 +08:00
Logic 7e53401a41 feat(web): unify three-signal investigation workspace 2026-07-15 22:02:34 +08:00
Logic 1a71153231 docs(collector): record intake license fix 2026-07-15 20:59:20 +08:00
Logic 4eab2fa3e1 fix(collector): preserve runtime dependency license 2026-07-15 20:59:05 +08:00
Logic 17cd27b79c docs(collector): record secure intake milestone 2026-07-15 20:56:53 +08:00
Logic 323f34b83b feat(collector): secure OTLP gateway intake 2026-07-15 20:56:38 +08:00
Logic 625c7b02c5 docs(collector): record active source milestone 2026-07-15 20:00:31 +08:00
Logic f27b2f184f feat(collector): govern managed telemetry sources 2026-07-15 20:00:12 +08:00
Logic ed24fd93a8 docs(collector): record resource governance milestone 2026-07-15 19:03:30 +08:00
Logic 652f2a8362 feat(collector): govern telemetry resources 2026-07-15 19:03:02 +08:00
Logic 3864c9cb01 docs(collector): fix complete delivery milestones 2026-07-15 18:22:51 +08:00
Logic 353aeadcd6 feat(collector): deliver managed runtime configuration 2026-07-15 18:22:17 +08:00
Logic 514175fd53 feat(collector): persist telemetry during outages 2026-07-15 16:59:32 +08:00
Logic 82d6e76c80 feat(collector): ingest application telemetry 2026-07-15 16:49:09 +08:00
Logic be6e8ccb3e build(collector): add hybrid release gates 2026-07-15 16:18:02 +08:00
Logic e49ecef01a feat(collector): report managed runtime state 2026-07-15 15:28:02 +08:00
Logic 709a030c7e feat(collector): collect prometheus metrics and file logs 2026-07-15 14:55:11 +08:00
Logic b07c7b3ca2 feat(web): complete log explore workflow 2026-07-15 14:34:09 +08:00
Logic 84983bc07d feat(collector): bind managed runtime intake identity 2026-07-15 14:26:49 +08:00
Logic 77644ff24d feat(web): build real metrics explore workspace 2026-07-15 14:23:14 +08:00
Logic e32ec3da66 build(collector): package hybrid runtime as native image 2026-07-15 14:09:00 +08:00
Logic 7b7eb919c9 feat(collector): make managed runtime activation recoverable 2026-07-15 14:08:43 +08:00
Logic 351d997ba7 fix(web): recover session after backend restart 2026-07-15 11:31:14 +08:00
Logic 130d02e82e feat(web): add shared signal explore workbench 2026-07-15 11:10:05 +08:00
Logic ce8b399dcb build: package managed OTel runtime 2026-07-13 16:59:09 +08:00
Logic ddd2ce401e feat: supervise managed OTel runtime 2026-07-13 16:58:46 +08:00
Logic 4f310a8764 feat: add notification channel services 2026-07-13 16:56:39 +08:00
Logic 69525c1d60 feat: add notification delivery rules 2026-07-13 16:44:58 +08:00
Logic 26775be49b feat: add minimal HertzBeat OTel runtime 2026-07-13 16:32:40 +08:00
Logic 3615519fdf feat: add notification template workflow 2026-07-13 16:31:45 +08:00
Logic 8722df9dea feat: add notification receiver workflow 2026-07-13 15:59:41 +08:00
Logic b0a34e1558 feat: add alert silence workflow 2026-07-13 15:44:08 +08:00
Logic 36e7509fc1 feat: add alert inhibition workflow 2026-07-13 15:26:14 +08:00
Logic 513bc0909b feat: add alert grouping workflow 2026-07-13 15:12:29 +08:00
Logic f64c408653 feat: add data router and alert rule workflows 2026-07-13 15:02:34 +08:00
Logic 7832ce6ba0 feat: complete monitor operations and metric detail 2026-07-13 10:48:58 +08:00
Logic c01f11bd7f feat: add lightweight alert center 2026-07-13 10:07:34 +08:00
Logic 4b3c7cebb3 feat: add monitor operator workflows 2026-07-13 09:59:54 +08:00
Logic 8e70f422a2 feat: add lightweight React operator shell 2026-07-13 09:00:18 +08:00
Logic ab18a8e275 feat: add secure React browser session boundary 2026-07-13 00:38:28 +08:00
Logic 7b73b0c195 feat: add lightweight React frontend foundation 2026-07-13 00:18:49 +08:00
Logic 876f867644 docs: define lightweight React frontend architecture 2026-07-13 00:06:32 +08:00
yuluo-yx 258fefd64f feat: fix 2.0 web frontend
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2026-07-11 00:00:36 +08:00
Logic 2de48d2caa feat: complete HertzBeat 2.0 novice workflows
Refine entity, monitor, alert, settings, observability, and shared operator workflows; add scale guards, route compatibility, release validation, and Product Design coverage.

AI assistance: used for draft implementation and test iteration.\nHuman validation: Node 22 production build; 426 frontend files and 3,398 tests; 59-route matrix; release budget 7.84 MiB / 8.00 MiB; focused backend tests and Browser QA.\nRisk notes: local scale proof covers 2,005 entities; sustained concurrent-ingestion SLA remains deployment-specific.
2026-07-10 14:54:43 +08:00
Logic 58a64fbcb2 Close HertzBeat beta workflows
AI assistance: used for draft implementation and test iteration.

Human validation: route/UI Lab/legacy audits; focused AI and alert Maven tests; in-app Browser beta smoke; i18n/CJK/diff checks.

Risk notes: backend-backed CRUD execution smokes still require running backend/provider data; local artifacts remain unstaged.
2026-06-17 11:01:10 +08:00
Logic 813d2f6fc9 Stabilize observability release smoke 2026-06-16 22:00:41 +08:00
Logic 79287fdeba Close entity observability loop
Add OTLP entity identity conflict governance, entity 360 evidence, monitor auto-bind refresh, topology identity relations, alert evidence handoffs, and entity unavailable state handling.

Validation: focused backend entity/topology/observability/alert tests passed; hertzbeat-startup package proof passed; focused frontend Vitest passed 19 files / 586 tests; ESLint, i18n report, diff checks, cached CJK scan passed; Playwright smoke covered overview, entities, entity detail, metrics, logs, traces, topology, and alert routes.

Risk notes: local smoke used Playwright fallback because Browser JS execution tool was unavailable; dev smoke still shows existing log-page maximum update depth console warnings and topology hydration mismatch without white screen or parameter loss.
2026-06-16 01:09:08 +08:00
Logic c979604973 Fix three-signal handoff queries 2026-06-15 02:01:38 +08:00
Logic 527f248106 Preserve resource related metric suggestions
Keep PromQL-proven related metric candidates while retaining pod and host resource-filter suggestions for cross-signal drilldowns.

Validation: ./mvnw -pl hertzbeat-observability -am '-Dtest=OtlpIngestionWorkspaceServiceImplTest#relatedMetricsPrefersPromqlAvailableCandidatesWhenExecutorExists+relatedMetricsKeepsResourceSuggestionsWhenPromqlFindsServiceCandidates' test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false; script/dev/run-three-signal-live-proof.sh; in-app Browser smoke for /ingestion/otlp/metrics, /log/manage, and /trace/manage.
2026-06-14 13:03:27 +08:00
Logic 8894b951df Support span name trace attribute filters 2026-06-10 09:37:16 +08:00
Logic b21e29fb73 Add operation filters to metric trace handoffs 2026-06-10 09:32:01 +08:00
Logic 9193e3bcfd Preserve metrics response operation handoffs 2026-06-10 09:28:57 +08:00
Logic a3e8dbe136 Expose metrics operation context 2026-06-10 09:25:36 +08:00
Logic 6756e4a071 Add operation filters to log trace handoffs 2026-06-10 09:18:10 +08:00
Logic 9d6bf8fed4 Add operation filters to metric log handoffs 2026-06-10 09:11:20 +08:00
Logic ee17b7a417 Preserve log operation context in handoffs 2026-06-10 09:07:38 +08:00
Logic 469251108e Add operation filters to trace log handoffs 2026-06-10 09:03:28 +08:00
Logic bd5edde109 Match trace operation filters against child spans 2026-06-10 08:57:58 +08:00
Logic 073dc37b7b Preserve metric series entity type context 2026-06-10 08:51:53 +08:00
Logic 0e1c69e54f Preserve metrics alert draft signal context 2026-06-10 08:48:13 +08:00
Logic c41a0b754b Preserve operation context in alert labels 2026-06-10 08:44:42 +08:00
Logic fda002fd64 Preserve metrics saved view operation context 2026-06-10 08:40:09 +08:00
Logic 6e3bc0e1f3 Preserve log stream attribute filters 2026-06-10 08:34:47 +08:00
Logic 6f059af2d7 Share trace related log context 2026-06-10 08:28:21 +08:00
Logic 1d3d4e3c8f Filter metrics console by operation context 2026-06-10 08:23:11 +08:00
Logic 03da23d1d5 Report actual related metric matchers 2026-06-10 08:11:29 +08:00
Logic e0bccccf0e Preserve trace operation handoff context 2026-06-10 08:07:11 +08:00
Logic 934350ee6a Preserve log operation in metrics preview 2026-06-10 08:02:00 +08:00
Logic fb89e34485 Enable source-only metrics inventory queries 2026-06-10 07:57:42 +08:00
Logic 713e003d69 Use source-backed OTLP metrics inventory 2026-06-10 07:53:55 +08:00
Logic 0ad8ffe098 Expose OTLP metrics inventory 2026-06-10 07:45:18 +08:00
Logic 04a3cdfef6 Discover related metric names from PromQL inventory 2026-06-10 00:13:08 +08:00
Logic b0f33f8bd4 Probe related metric candidates with PromQL 2026-06-10 00:08:21 +08:00
Logic e50af063e5 Compact trace attribute operator actions 2026-06-10 00:00:22 +08:00
Logic db3adbf8b6 Compact log attribute operator actions 2026-06-09 23:47:29 +08:00
Logic 9d2aeddbb7 Compact metrics attribute operator actions 2026-06-09 23:38:22 +08:00
Logic 9adc23e78a Add signal list filter actions 2026-06-09 23:25:23 +08:00
Logic adf3000b24 Add signal not-contains filter actions 2026-06-09 23:12:00 +08:00
Logic 89bb210d29 Add signal contains filter actions 2026-06-09 23:03:16 +08:00
Logic e29a823a9c Add signal not-exists filter actions 2026-06-09 22:56:30 +08:00
Logic 45866c886f Expose log resource attribute filters 2026-06-09 22:47:23 +08:00
Logic 74db8573eb Expose signal filter operator hints 2026-06-09 22:38:30 +08:00
Logic b4eaeda5b0 Add signal attribute exists actions 2026-06-09 22:32:23 +08:00
Logic 8b2925951f Add metrics label exclude actions 2026-06-09 22:25:16 +08:00
Logic b76ac05048 Add trace attribute exclude actions 2026-06-09 22:21:16 +08:00
Logic 6650d995e5 Support trace span attribute group by 2026-06-09 22:12:57 +08:00
Logic 22549189ac Add trace span attribute filter actions 2026-06-09 22:02:40 +08:00
Logic 06f2966639 Expose trace span attribute filters 2026-06-09 21:58:40 +08:00
Logic 7d11789354 Support trace span attribute filters 2026-06-09 21:51:24 +08:00
Logic b017cebc2b Support related metrics friendly filters 2026-06-09 21:43:19 +08:00
Logic b5e4ca5142 Support metrics friendly label filters 2026-06-09 21:39:48 +08:00
Logic 744685644c Support trace resource text filters 2026-06-09 21:34:34 +08:00
Logic ea6a637847 Support log exists filters 2026-06-09 21:28:25 +08:00
Logic e635f50580 Support log contains filters 2026-06-09 21:23:45 +08:00
Logic ae53af9187 Support log trace coverage IN filters 2026-06-09 21:19:55 +08:00
Logic cba4b12f5b Support log group-by IN filters 2026-06-09 21:16:46 +08:00
Logic 6527b579b3 Support log trend IN filters 2026-06-09 21:12:02 +08:00
Logic 5c95a18f69 Support log overview IN filters 2026-06-09 21:03:17 +08:00
Logic f339ade838 Support log context IN filters 2026-06-09 21:00:15 +08:00
Logic 499130500b Support log list IN filters 2026-06-09 20:56:40 +08:00
Logic fed439e5d8 Support trace negative resource filters 2026-06-09 20:49:13 +08:00
Logic ada22b22c9 Support trace resource filter IN clauses 2026-06-09 20:41:47 +08:00
Logic 165a33a470 Scope trace service by entity id 2026-06-09 20:36:52 +08:00
Logic 65b0dbacca Scope log queries by entity id 2026-06-09 20:32:51 +08:00
Logic 5948adf0df Scope trace queries by entity id 2026-06-09 20:26:43 +08:00
Logic 12e7966dd6 Scope metrics console queries by entity id 2026-06-09 20:23:14 +08:00
Logic 5c8bc5914d Preserve entity context in live log streams 2026-06-09 20:18:53 +08:00
Logic 5fda65c4b0 Use response handoffs for entity signal routes 2026-06-09 20:14:44 +08:00
Logic a306dfc748 Carry entity context in response handoffs 2026-06-09 20:07:46 +08:00
Logic ed0c191325 Enrich signal evidence snapshots with entity context 2026-06-09 20:02:47 +08:00
Logic 9c4e8d93f2 Record entity context in signal evidence payloads 2026-06-09 19:57:56 +08:00
Logic 97e0ecee7e Preserve drilldown filters in direct signal links 2026-06-09 19:51:53 +08:00
Logic 76e97c23b4 Record drilldown metadata in breakout drafts 2026-06-09 19:48:21 +08:00
Logic 14687f8f3c Record drilldown metadata in evidence drafts 2026-06-09 19:45:44 +08:00
Logic 86a3f44391 Preserve attribute filters from signal evidence 2026-06-09 19:42:49 +08:00
Logic db36543e15 Suggest resource filters from signal evidence 2026-06-09 19:38:50 +08:00
Logic fbbf6c7f80 Suggest operation filters from signal evidence 2026-06-09 19:35:02 +08:00
Logic 690700b094 Preserve evidence drilldown filters in signal handoffs 2026-06-09 19:30:06 +08:00
Logic 1060b5a914 Preserve metric route context in signal handoffs 2026-06-09 19:22:24 +08:00
Logic db46f8d13d Suggest entity type in signal evidence filters 2026-06-09 19:11:46 +08:00
Logic 4283a1c29f Preserve environment context in signal handoffs 2026-06-09 19:06:51 +08:00
Logic 1d751a4196 Preserve metric entity context in trace handoffs 2026-06-09 18:58:15 +08:00
Logic a7b4cb4aca Preserve RPC service in metric drilldowns 2026-06-09 18:48:57 +08:00
Logic dc381f1ae3 Align trace span group drilldown 2026-06-09 18:40:48 +08:00
Logic d58654052f Align trace target group drilldown 2026-06-09 18:34:53 +08:00
Logic 061385a6a3 Align trace route group drilldown 2026-06-09 18:27:44 +08:00
Logic 0dc8c850a6 Align log attribute group drilldown 2026-06-09 18:22:41 +08:00
Logic d61e8f13c1 Align trace operation group drilldown 2026-06-09 18:16:31 +08:00
Logic 0a82473074 Gate frontend verify on three-signal cutoff 2026-06-09 18:04:04 +08:00
Logic 8f85633368 Add three-signal alpha cutoff evidence report 2026-06-09 18:00:20 +08:00
Logic 4516009dbd Document three-signal alpha cutoff 2026-06-09 17:55:52 +08:00
Logic 62941f3322 Add operation drilldown live proof 2026-06-08 10:56:30 +08:00
Logic faf056464b Add operation context to related metrics 2026-06-08 10:55:03 +08:00
Logic 1eeed55335 Wire operation drilldown dashboard action 2026-06-08 10:10:07 +08:00
Logic 34447423ef Add operation drilldown dashboard builder 2026-06-08 09:56:27 +08:00
Logic 40df81961c Cover service operation trace drilldown 2026-06-08 09:52:15 +08:00
Logic 0e82832c46 Guard signal runtime dimensions from entity identity 2026-06-08 09:45:13 +08:00
Logic ceedadc3bd Add service overview live proof 2026-06-08 09:39:08 +08:00
Logic b7a61f1471 Add service overview dashboard browser smoke 2026-06-08 09:25:29 +08:00
Logic d58a839f4b Wire service overview dashboard action 2026-06-08 09:18:08 +08:00
Logic 3d2b941baa Add service overview dashboard contract 2026-06-08 09:12:28 +08:00
Logic 0cf07ee633 Add three-signal frontend workspace flows 2026-06-08 09:03:10 +08:00
Logic 8c2e22958e Add three-signal backend workspace contracts 2026-06-08 08:54:32 +08:00
Logic b3bd1814f2 Fix Spring AI 2 startup compatibility 2026-06-07 10:32:44 +08:00
Logic 5b4cf11fe1 Prevent Spring AI auto chat startup 2026-06-06 22:50:12 +08:00
Logic 721e53d206 fix(ai): upgrade spring ai and keep chat client optional 2026-06-06 18:34:18 +08:00
Logic 8b72b8a0ff feat(observability): preserve signal alert filters 2026-06-06 18:33:37 +08:00
Logic c0671acf92 feat(observability): improve signal explorer parity
Add logs, traces, and metrics explorer parity improvements including backend query contracts, frontend route state, export helpers, and alert handoff coverage.

AI assistance: used for draft implementation and test iteration.

Human validation: ./mvnw -pl hertzbeat-ai -Dtest=LlmConfigTest,ChatClientProviderServiceImplTest test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false; ./mvnw -pl hertzbeat-startup -am -DskipTests package; ./mvnw -pl hertzbeat-startup -Dtest=ContextTest test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false; npm run i18n:report; git diff --check; git diff --cached --check.

Risk notes: broader Signoz-like parity still needs saved views, notification workflow handoff, dashboard panel APIs, richer log query parsing, and browser smoke.
2026-06-06 17:30:58 +08:00
Logic 130d446664 Merge remote-tracking branch 'apache/2.0.0' into 2.0.0 2026-06-06 15:13:10 +08:00
Logic e656c11712 fix(ai): skip chat client when provider credentials are missing 2026-06-06 15:13:05 +08:00
liuhyandClaude Opus 4.8 20caf89894 fix(core): replace silent exception swallowing and assert-based null guards
- MonitorServiceImpl: log warning when detectMonitor fails during addMonitor
  and reapplyMonitors instead of silently swallowing the exception
- TdEngineDataStorage: replace 4 'assert x != null' with 'if (x != null)'
  guards in finally blocks. Java assertions are disabled by default (-da),
  meaning assert was a no-op and NullPointerException from the subsequent
  close() call would mask the original exception

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:26:28 +08:00
liuhyandClaude Opus 4.8 3747e33e7c fix(concurrency): fix race conditions and interrupt flag handling
- CollectorJobScheduler: register response listener BEFORE sending message
  to prevent silent data loss when response arrives before registration;
  add finally block to always clean up listener from eventListeners map
- ConsistentHash: add synchronized to addNode/removeNode/dispatchJob/
  preDispatchJob to prevent concurrent modification of hashCircle and
  existNodeMap during collector rebalancing
- Metrics: replace synchronized(subTaskNum) with dedicated subTaskLock
  object to prevent NPE when AtomicInteger is null after deserialization
- NettyRemotingAbstract: replace containsKey()+get() with single get()+null
  check to eliminate TOCTOU race with sendMsgSyncImpl's finally block;
  restore interrupt flag on InterruptedException
- NettyRemotingClient/Server: restore Thread.interrupt() flag after
  catching InterruptedException to preserve shutdown signal

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:26:19 +08:00
liuhyandClaude Opus 4.8 d1e533f23d fix(collector): fix resource leaks in WebSocket, script, FTP, and JDBC collectors
- WebsocketCollectImpl: add finally block to close socket and I/O streams on
  all code paths (previously only closed on success, leaking on exception)
- ScriptCollectImpl: add finally block to close BufferedReader and destroy
  Process; restore interrupt flag on InterruptedException
- FtpCollectImpl: add finally block to disconnect FTPClient on exception
- JdbcCommonCollect: close JDBC Connection if setReadOnly/createStatement
  fails (previously leaked because statement remained null and caller's
  finally block skipped cleanup)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:26:08 +08:00
liuhyandClaude Opus 4.8 b7f7167070 fix(alerter): add null guards for commonLabels/commonAnnotations across SMS clients
- UniSmsClientImpl: add null check for getCommonLabels() and getCommonAnnotations()
  to match pattern used by other 4 SMS clients (Alibaba, Aws, Tencent, Twilio)
- All SMS clients: initialize instance/priority with safe defaults before
  null-guarded access, use getOrDefault for cleaner null handling
- SmsLocalSmsClientImpl: add null guards for getCommonAnnotations() and getAlerts()
- NoticeConfigServiceImpl: add null check for getCommonLabels() in rule filter

These maps can legitimately be null when alerts originate from external
sources that don't populate labels/annotations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:25:54 +08:00
liuhyandClaude Opus 4.8 c23be1b5b7 fix(arrow): fix RootAllocator native memory leaks in Arrow serialization
- CollectRep: bind allocator lifecycle to MetricsData, close on MetricsData.close()
- ArrowUtil: create per-root allocator in deserializeMetricsData so each
  MetricsData can be independently closed without corrupting others
- KafkaMetricsDataDeserializer: pass allocator to MetricsData instead of
  leaking it on every deserialization call
- ArrowUtilTest: update test to match new deserializeMultipleRoots API

RootAllocator allocates off-heap (native) memory via Arrow. Previously the
allocator was never closed on success paths, causing unbounded native memory
growth with every metrics collection cycle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:25:43 +08:00
liuhyandClaude Opus 4.8 343a5dcf20 fix(i18n): correct locale priority and fix Japanese label key
- Reorder locale resolution priority: localStorage > backend API > browser
- Fix Japanese locale labelKey from 'ja-JP' to 'ja_JP' for consistency

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:58:23 +08:00
yuluo-yx ad52459f8d chore: delete unuse test
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2026-06-04 23:29:07 +08:00
yuluo-yx 8db769a308 fix: fix compile error
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2026-06-04 23:12:45 +08:00
tomsun28 682caa82ae fix spring-ai
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2026-06-04 22:54:25 +08:00
Logic 5356dd523a Fix Spring AI blank credential startup 2026-06-04 22:45:00 +08:00
tomsun28 35e373ee37 fix spring-ai
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2026-06-04 22:25:12 +08:00
Logic 9c69aab450 refactor(web-next): align UI Lab ownership and dark workbenches
Add a route-catalog UI Lab coverage report and close the remaining primary-page ownership gaps with shared source-doc and exception surfaces.

Burn down cold/local owner markers across shared frontend primitives, expand i18n reporting for missing runtime keys, and keep overview plus log/trace/metrics on hard-edge dark workbench chrome.

Validation: npm exec -- vitest run scripts/ui-lab-coverage-report.test.ts packages/hertzbeat-ui/src/index.test.tsx app/ui-lab/page.test.tsx app/overview/page.test.tsx app/log/manage/page.test.tsx app/trace/manage/page.test.tsx app/trace/manage/trace-manage-client.test.tsx app/ingestion/otlp/metrics/page.test.tsx 'app/alert/integration/[source]/page.test.tsx' 'app/exception/[type]/page.test.tsx' components/pages/exception-center-surface.test.tsx --pool=forks --maxWorkers=1 --minWorkers=1 --testTimeout=120000 --hookTimeout=120000; npm run i18n:report; node scripts/ui-lab-coverage-report.mjs; frontend CJK scan; git diff --cached --check.
2026-06-04 09:01:57 +08:00
Logic 4ef6e7e23d fix(web-next): keep legacy default theme dark 2026-06-03 19:29:12 +08:00
Logic 802ab06db4 fix(startup): keep entity foundation migration in V200 2026-06-03 18:45:16 +08:00
Logic cd0b0a4c37 feat(web-next): finalize alpha localization cleanup 2026-06-03 11:42:10 +08:00
Logic 3832485f0f refactor: externalize backend localization messages 2026-06-03 11:40:48 +08:00
Logic fc5c12431e chore(frontend): retire angular web app 2026-06-03 11:39:29 +08:00
Logic d068e0a73a Prepare HertzBeat community alpha preview
Add alpha preview docs and validation checklist.

Harden entity topology, signal evidence, and Greptime trace read models.

Add Next topology/entity shell surfaces with shared UI and G6 smoke coverage.

Fix hertzbeat-ai Maven child version model warning.
2026-06-02 21:29:25 +08:00
Logic 7ff8e34685 fix(web-next): clamp compact topology zoom after centering 2026-05-31 10:37:07 +08:00
Logic 6afe1a7f0e fix(web-next): keep compact topology graphs at 1x 2026-05-31 10:26:42 +08:00
Logic 04aa6d5fdf fix(web-next): bound topology operator zoom 2026-05-31 10:05:13 +08:00
Logic f47ac7311f fix(web-next): surface topology windowed metric table 2026-05-31 09:50:20 +08:00
Logic 23523355b7 fix(web-next): keep topology search input explicit 2026-05-31 09:33:25 +08:00
Logic 69b98c9375 fix(web-next): keep topology selection out of render window 2026-05-31 09:22:00 +08:00
Logic c0eb3a0814 fix(web-next): source topology node icons from catalog 2026-05-31 09:04:54 +08:00
Logic 64f7dd69a2 fix(web-next): dedupe topology header scope summary 2026-05-31 08:54:09 +08:00
Logic 4e969d7bcc fix(web-next): cap topology auto-fit zoom 2026-05-31 08:41:19 +08:00
Logic b8f13bffe9 fix(web-next): dedupe topology focus filters 2026-05-31 08:30:10 +08:00
Logic 1d6164c2d5 fix(web-next): tighten topology toolbar alignment 2026-05-31 08:23:09 +08:00
Logic 446b18b603 fix(web-next): suppress empty topology legends 2026-05-31 08:06:19 +08:00
Logic 72ddbb8020 fix(web-next): scope topology legend to visible graph types 2026-05-29 22:08:23 +08:00
Logic 0e284d69c3 feat(web-next): expose source-backed topology node legend 2026-05-29 21:58:31 +08:00
Logic 81c4bd6576 fix(web-next): remove hand-drawn topology legend marks 2026-05-29 21:46:12 +08:00
Logic eb5c666d8d test(web-next): align topology toolbar fit metadata 2026-05-29 21:36:10 +08:00
Logic 202f407d2c fix(web-next): stop topology first-paint overfit 2026-05-29 21:33:24 +08:00
Logic 43b8d665f3 fix(web-next): align alert setting import lifecycle 2026-05-29 15:52:23 +08:00
Logic 7e2b3582e1 test(web-next): add HertzBeat 2.0 gap and ecosystem gates 2026-05-29 15:18:47 +08:00
Logic 16dd2828b4 feat(manager): add entity workbench backend models
Validation:

- ./mvnw -pl hertzbeat-manager -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests validate

- ./mvnw -pl hertzbeat-manager -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests clean compile

- ./mvnw -pl hertzbeat-observability,hertzbeat-manager -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests compile

- ./mvnw -pl hertzbeat-observability,hertzbeat-manager -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false -Dtest='ObserveEntityServiceTest,MonitorServiceTest,MonitorControllerTest,MonitorsControllerTest,ManagerObservabilityWorkspaceQueryGatewayTest,ManagerObservabilityInventoryQueryServiceTest,*Entity*Test,*OldMonitor*Test,*TraceCallTopology*Test,ActionCatalogServiceTest,ActionApprovalDraftServiceTest,TopologyControllerTest,MonitorRouteAuthorizationConfigTest,AppServiceStartupGuardSourceTest' test

- git diff --cached --check

- cached trailing-whitespace scan
2026-05-29 10:14:20 +08:00
Logic 778011d219 fix(remoting): use background task executor
Validation:

- ./mvnw -pl hertzbeat-remoting -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests clean compile

- git diff --cached --check

- cached trailing-whitespace scan
2026-05-29 09:51:45 +08:00
Logic 22d4574d54 fix(collector): include mysql r2dbc runtime
Validation:

- ./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests clean compile

- git diff --cached --check

- cached trailing-whitespace scan
2026-05-29 09:49:15 +08:00
Logic 66ae2f6c88 fix(ai): align tools with backend API changes
Validation:

- ./mvnw -pl hertzbeat-ai -Dcheckstyle.skip=true -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests clean compile

- git diff --cached --check

- cached trailing-whitespace scan
2026-05-29 09:46:56 +08:00
Logic f094cf7a2a feat(backend): harden OTLP ingestion and token scopes
Validation:

- ./mvnw -pl hertzbeat-common-core,hertzbeat-common-spring,hertzbeat-warehouse,hertzbeat-observability -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DskipTests compile

- ./mvnw -pl hertzbeat-common-core,hertzbeat-common-spring,hertzbeat-warehouse,hertzbeat-observability -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false -Dtest='OtlpWorkspaceDtoMigrationTest,EntitySchemaLegacyColumnCompatibilityTest,GreptimeSqlQueryExecutorTest,GreptimeTraceQueryRepositoryTest,OpenTelemetryConfigTest,OtlpLogProtocolAdapterTest,OtlpGrpcServerConfigTest,OtlpIngestionControllerTest,OtlpLogControllerTest,OtlpLogControllerSourceOwnershipTest,OtlpSignalIngestionControllerTest,OtlpCorrelationEnricherTest,OtlpEntityIdentityResolverTest,GreptimeApmFlowInitializerTest,GreptimeLogPipelineInitializerTest,GreptimeOtlpForwarderTest,GreptimeTraceTableInitializerTest,OtlpGrpcIngestionServiceImplTest,OtlpIngestionWorkspaceServiceImplTest,LogQueryControllerTest,LogSseControllerTest,TelemetryIntakeServiceImplTest,EntityTraceQueryServiceImplTest,*Audit*Test,*Governance*Test,*Quota*Test,*Redaction*Test,*Retry*Test,*Security*Test,*Semantic*Test,*RedSummary*Test,*ErrorResponse*Test' test

- ./mvnw -pl hertzbeat-startup -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -Dtest=ReleaseReadinessRuntimeAssemblyTest test

- ./mvnw -pl hertzbeat-manager -Dcheckstyle.skip=true -DskipITs -Dskip.frontend -Dskip.npm -DskipDocker -Dtest='ApiTokenValidationFilterTest,AuthTokenControllerTest,AccountServiceTest,OtlpRouteAuthorizationConfigTest' test

- git diff --cached --check
2026-05-29 09:34:43 +08:00
Logic c4d242fa6a feat(web-next): add M10 frontend convergence gate 2026-05-29 02:35:16 +08:00
Logic aac09ca2a3 feat(web-next): close M10 route cutover audit 2026-05-29 02:29:03 +08:00
Logic bf2a36868b feat(web-next): add release runtime readiness gates 2026-05-29 02:14:08 +08:00
Logic 435cc244aa feat(web-next): align shared UI primitive parity
Validation: vitest run app/loading.test.tsx packages/design-tokens/src/tokens.test.ts components/ui/cold-code-editor.test.tsx components/ui/cold-confirm-dialog.test.tsx components/ui/date-time-range.test.tsx components/ui/label-record-input.test.tsx components/ui/number-stepper.test.tsx components/ui/search-row.test.tsx components/ui/select.test.tsx components/ui/tag-input.test.tsx components/workbench/overlay-dialog.test.tsx components/workbench/workspace-tab-strip.test.tsx app/ui-lab/page.test.tsx --pool=forks --maxWorkers=1 --minWorkers=1; ESLint same TS/TSX shared UI set; git diff --cached --check; cached trailing-whitespace scan. Browser proof blocked by no active Codex browser pane.
2026-05-29 02:08:31 +08:00
Logic 95c7899533 feat(web-next): restore bulletin workbench compatibility
Validation: vitest run app/bulletin/page.test.tsx components/pages/bulletin-center-surface.test.tsx components/pages/bulletin-manage-dialog.test.tsx components/pages/bulletin-metrics-table.test.tsx components/pages/bulletin-monitor-detail.chrome.test.ts lib/bulletin-center/controller.test.ts lib/bulletin-center/query-state.test.ts lib/bulletin-center/view-model.test.ts --pool=forks --maxWorkers=1 --minWorkers=1; ESLint same bulletin route/component/lib set; git diff --cached --check; cached trailing-whitespace scan. Browser proof blocked by no active Codex browser pane.
2026-05-29 02:02:27 +08:00
Logic cddb70aa56 feat(web-next): close dashboard route catalog parity 2026-05-29 01:51:04 +08:00
Logic a571f59693 feat(web-next): restore observability workbench parity 2026-05-29 01:39:28 +08:00
Logic 5e3edd3f41 feat(web-next): restore entity workbench parity 2026-05-29 01:26:51 +08:00
Logic 585626c269 feat(web-next): restore status actions parity 2026-05-29 01:20:40 +08:00
Logic c64801e73d feat(web-next): restore passport parity 2026-05-29 01:14:54 +08:00
Logic 2991c30489 feat(web-next): restore settings platform parity 2026-05-29 01:07:28 +08:00
Logic cae4d75765 feat(web-next): restore collector plugin label parity 2026-05-29 01:02:59 +08:00
Logic 0f6c22d9c7 feat(web-next): restore alert workflow parity 2026-05-29 00:57:14 +08:00
Logic e403551b18 feat(web-next): restore overview dashboard parity 2026-05-29 00:45:37 +08:00
Logic f114f1c6d1 feat(web-next): restore shell utility parity 2026-05-29 00:39:49 +08:00
Logic a6723173fd feat(web-next): expand runtime i18n catalog 2026-05-29 00:38:17 +08:00
Logic 457076998a feat(web-next): restore monitor detail parity 2026-05-29 00:31:21 +08:00
Logic d21cadcbe3 feat(web-next): restore monitor list parity 2026-05-29 00:27:52 +08:00
Logic 388b76446e feat(web-next): restore monitor editor parity 2026-05-29 00:21:14 +08:00
Logic 584fca5f0d feat(web-next): add client runtime foundation 2026-05-29 00:08:52 +08:00
Logic 01c57a50d3 feat(web-next): add shared UI lab foundation 2026-05-28 23:58:25 +08:00
Logic f6ca255355 feat: add web-next observability migration 2026-05-04 00:40:12 +08:00
Logicandlynx009 9e1a460dc7 refactor: update to Springboot4 and Hibernate 7 (#4057)
Co-authored-by: lynx009 <2030509072@qq.com>
2026-03-09 23:34:15 +08:00
Tomsun28 00b83ff531 chore: remove the sharp package (#4056)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2026-03-08 20:01:49 +08:00
shown 5c2950d838 feat[common]: add protocl entity check (#4055)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2026-03-08 19:31:18 +08:00
brettgervasoniandBrett 155667833e [bugfix] correct typo in sureness monitor delete verb definition (#4054)
Co-authored-by: Brett <>
2026-03-08 15:32:35 +08:00
Tomsun28 6b78364596 docs: improve website geo (#4053) 2026-03-07 16:30:44 +08:00
shownandTomsun28 35b79cf649 feat[common]: add hertzbeat common protocol entitiy check (#4052)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-03-07 11:56:12 +08:00
fc2efb13eb style: apply primary button style to monitor manage action (#4045)
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-03-07 11:54:35 +08:00
DuansgandTomsun28 84361b4d87 [improve] improved interaction styling for the template editor (#4051)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-03-04 23:34:05 +08:00
DuansgandTomsun28 40c842b0d7 [improve] add group convergence strategy name validation (#4050)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-03-04 23:30:01 +08:00
shownandDuansg f76581b860 fix[protocol-sd]: add register center params check (#4048)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
2026-03-04 23:19:00 +08:00
LunaRain_079 b3dba8b3cf [feat] Support Apache Doris as time-series storage for both metrics and logs (#4031) 2026-03-02 23:02:51 +08:00
layla 8174b68c0d Fix typo: occured to occurred (#4049) 2026-03-01 22:35:50 +08:00
Tomsun28andshown dea7bdc7fe fix: fix home doc deploy action (#4029)
Co-authored-by: shown <yuluo08290126@gmail.com>
2026-03-01 19:37:09 +08:00
Tomsun28andshown c165be6c2a docs: update security model desc (#4047)
Co-authored-by: shown <yuluo08290126@gmail.com>
2026-03-01 19:05:50 +08:00
d99b5dbff5 feat: SOP-driven AI workflows with skills, scheduling, and i18n support (#4016)
Signed-off-by: aias00 <liuhongyu@apache.org>
Signed-off-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: lynx009 <2030509072@qq.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-03-01 15:53:24 +08:00
DuansgandTomsun28 cc50a50a0d [fix] fix alertmanager integration issue (#4022)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-02-28 00:10:50 +08:00
Duansgandliutianyou dbcd3f3c0d [improve] Improve the login page style (#4044)
Co-authored-by: liutianyou <liutianyou@apache.org>
2026-02-27 23:11:45 +08:00
67c11b9128 fix: Add i18n support for setting-drawer component (#4006)
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: liutianyou <liutianyou@apache.org>
2026-02-27 22:48:44 +08:00
cd40699293 refactor: Split hertzbeat-common into core and spring modules (#4035)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: zqr10159 <30048352+zqr10159@users.noreply.github.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-02-25 09:49:52 +08:00
DuansgandLogic 70015568e2 Revert "[Task] Add user account monitoring metrics for MongoDB" (#4038)
Co-authored-by: Logic <zqr10159@dromara.org>
2026-02-24 22:18:43 +08:00
767ce6b073 docs: add H2 database security warning page (#4036)
Signed-off-by: Turan Almammadov <16321061+turanalmammadov@users.noreply.github.com>
Co-authored-by: Turan Almammadov <16321061+turanalmammadov@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 22:00:29 +08:00
09818174eb [Task] Add user account monitoring metrics for MongoDB (#4033)
Signed-off-by: Turan Almammadov <16321061+turanalmammadov@users.noreply.github.com>
Co-authored-by: Turan Almammadov <16321061+turanalmammadov@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 17:17:16 +08:00
Duansg ebfd3c73e3 [fix] Fixed template name mismatch issue (#4021) 2026-02-13 23:06:02 +08:00
Tomsun28 cc60f8453b docs: fix the sidebars i18n and version display, add auto deploy action (#4018)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2026-02-08 22:14:05 +08:00
0cf286a063 [Docs] Release version 1.8.0 documentation (#4017)
Signed-off-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-07 10:18:41 +08:00
3da977a96f docs: update download page to v1.8.0 (#4015)
Co-authored-by: a-little-fool <2030509072@qq.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-02-07 09:01:49 +08:00
Duansg c10761ec9a [improve] improved version validation documentation content (#4013) 2026-02-06 20:50:59 +08:00
Duansg b0c6324713 [fix] Fixed Prometheus task history charts not working in the Favorites tab (#4014) 2026-02-06 16:39:01 +08:00
DuansgandTomsun28 1b51c19c97 [fix]fixed sql formatting error (#4008)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-01-30 00:00:31 +08:00
LunaRain_079 a83cb0f50f [docs] Fix HTTP Service Discovery API response format (#4005) 2026-01-29 22:10:15 +08:00
Tomsun28 d6022cb587 docs: update doc add contributors and pnpm lock (#4002)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2026-01-25 20:34:49 +08:00
LogicandTomsun28 67f525a603 [fix] update table name generation to include application context (#4001)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2026-01-24 23:26:08 +08:00
LunaRain_079andDuansg 634006aa7e [refactor] Dynamically concatenate app parameter on the front end to accommodate Prometheus apps (#4000)
Co-authored-by: Duansg <siguoduan@gmail.com>
2026-01-24 23:06:25 +08:00
3eb3f926f5 [fix] Update query parameter encoding and error time parser in VictoriaMetricsClusterDataStorage (#3989)
Co-authored-by: Yang Chen <1597081640@qq.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2026-01-24 20:37:08 +08:00
47caa5bdf8 feat: support otlp http binary protobuf format log data and update doc (#3986)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Logic <zqr10159@dromara.org>
2026-01-23 18:34:36 +08:00
Logic 67c3727362 [Refactor] tighten resource constraints for XML/XPath collector (#3999) 2026-01-22 18:51:01 +08:00
b510d6e5ea [feature] add datasource status retrieval and update alert settings UI (#3985)
Signed-off-by: Logic <zqr10159@126.com>
Signed-off-by: Duansg <siguoduan@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
2026-01-20 22:43:47 +08:00
1ab27bd556 [doc]: update to Docusaurus v3.9.2 (#3984)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2026-01-20 22:04:27 +08:00
Duansg 9600a5e56f [improve] upgrade script merge (#3987) 2026-01-19 23:47:32 +08:00
6c0d5c0db4 [fix]Fix issues such as index failure (#3953)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: Yang Chen <1597081640@qq.com>
Co-authored-by: Logic <zqr10159@dromara.org>
2026-01-18 23:07:42 +08:00
4b010e6523 [monitoring-template] feat(mysql): add database account expiry metrics (#3969)
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2026-01-18 22:29:03 +08:00
8962253dd2 [manager] bugfix: fix incorrect status page date calculation (#3962)
Signed-off-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-17 20:08:12 +08:00
Logic afd89e4309 Fix htaccess (#3980)
Signed-off-by: Logic <zqr10159@dromara.org>
2026-01-16 22:16:15 +08:00
Logic 43332f49db [fix]: update Content Security Policy in .htaccess to include new domains (#3979) 2026-01-16 20:53:11 +08:00
Duansg cc90de5541 [fix] fixed cache parsing issues (#3977) 2026-01-16 15:47:09 +08:00
Tomsun28 5e1460bf29 docs: update invitation email template (#3975) 2026-01-16 09:10:51 +08:00
LogicandYang Chen 7dbdca849a [doc]: add monitoring documentation for various service discovery methods (#3965)
Co-authored-by: Yang Chen <1597081640@qq.com>
2026-01-15 21:27:42 +08:00
Yang ChenandDuansg df525d2d32 fix: add GreptimeDB docker-compose assembly configuration (#3960)
Co-authored-by: Duansg <siguoduan@gmail.com>
2026-01-14 20:51:24 +08:00
LunaRain_079 dc96f1766b [Fix]: fix TAG type mismatch by introducing schema version for TDengine (#3963) 2026-01-13 21:29:27 +08:00
Tomsun28andDuansg 70da7d5dce docs: update docs link (#3958)
Co-authored-by: Duansg <siguoduan@gmail.com>
2026-01-11 18:46:13 +08:00
Prakash Kumar 0f1169ce36 [Fix] Fixed alignment of header social links on mobile (#3959) 2026-01-11 09:57:39 +08:00
KOYR 1c38d01abb style: widen table column to prevent label truncation (#3957) 2026-01-10 22:10:53 +08:00
f82b5add11 [feature] Update CSP permissions to include Kapa.ai services (#3948)
Signed-off-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 09:49:48 +08:00
KOYR 8e2c1f3b07 style: adjust grid layout width of form labels (#3950) 2026-01-07 15:06:45 +08:00
Logic 8ef92d70d2 [fix] Update overflow properties and enhance swiper container styles (#3947) 2026-01-07 08:10:37 +08:00
Yang Chen b5ce5c9e9d [fix] refactor system built-in labels validation logic (#3946) 2026-01-06 15:47:25 +08:00
P_Peaceful a19a399acd [bugfix&improve] Fix alert label persistence and improve custom label input (#3933) 2026-01-06 12:01:53 +08:00
529d161478 [improve] Optimize Tooltip display logic and support scrollable content (#3941)
Signed-off-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-04 23:24:46 +08:00
168f78ef23 chore: optimzie docker compose docs hertzbeat version (#3929)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2026-01-03 19:05:35 +08:00
KOYRandDuansg a159127572 [improve]: Optimize yAxis scale to better reflect data fluctuations (#3939)
Co-authored-by: Duansg <siguoduan@gmail.com>
2026-01-03 01:57:14 +08:00
88933ce0bb [feature] Update UI text and styles for website (#3934)
Signed-off-by: Logic <zqr10159@126.com>
Signed-off-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-12-31 16:00:13 +08:00
Logic 6d703f7eae [feature] add Kapa widget script to docusaurus configuration (#3938) 2025-12-31 10:30:49 +08:00
Duansg f4a0fe3445 [fix] fixed CSP blocking algolia from loading on the page (#3935) 2025-12-30 09:56:58 +08:00
Sahil Shadwal b174c8622c [bugfix]: fix for the collector tab click effect error (#3932) 2025-12-28 18:42:04 +08:00
LathikaBaddam 98e0412ee6 [docs] improve English translation and technical terms for MySQL (#3928) 2025-12-27 20:20:04 +08:00
Logic a138238994 [bugfix] Enhance expiration time parsing to support strict day format (#3924) 2025-12-22 20:44:21 +08:00
Duansgandlynx009 01f5cb1fd6 [feat] Add Grafana to follow the system theme (#3920)
Co-authored-by: lynx009 <2030509072@qq.com>
2025-12-20 19:44:53 +08:00
Tomsun28andYang Chen 472554bc55 [chore] update docker compose hertzbeat version (#3916)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Yang Chen <1597081640@qq.com>
2025-12-19 14:15:28 +08:00
Logic ad9a4ab100 [improve] xugu.md and app-xugu.yml (#3918) 2025-12-17 18:51:58 +08:00
KOYR a806ca1319 [Feature] Enhance draggable UI elements in alert settings (#3915) (#3917) 2025-12-17 09:33:23 +08:00
P_PeacefulandTomsun28 aac5bafe43 [bugfix]: AI conversation message loading issue (#3911)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-16 23:54:38 +08:00
P_PeacefulandLogic 668106d445 [bugfix] AI prompt classpath error (#3912)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-12-16 11:36:23 +08:00
a74671abdd [improve]Improve UI handling for label overflow (#3913)
Co-authored-by: 半半 <kongranyi@cai-inc.com>
Co-authored-by: Logic <zqr10159@dromara.org>
2025-12-16 11:03:15 +08:00
Duansg 120d494c47 [fix]Fixed private collector tasks not taking effect (#3914) 2025-12-16 10:51:39 +08:00
DeleiGuo 5b1d01e0c5 [bugfix] Modify the search predicate from 'host' to 'instance' field (#3910) 2025-12-13 10:25:52 +08:00
Yang ChenandCopilot 950138a340 [feat] Add an SQL editor and prevent SQL injection (#3900)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 17:52:31 +08:00
zhaoyangplus 93d8f92165 feat(db): Added support for monitoring the Xugu database (#3899) 2025-12-12 11:32:40 +08:00
LogicandCopilot 488df340a1 [Fix]: Use a connection pool to avoid concurrent access to duckdb files. (#3898)
Signed-off-by: Logic <zqr10159@126.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 15:38:30 +08:00
DuansgandTomsun28 dc51ce7bef [fix]Fixed position offset issue during deserialization of ArrowUtil (#3897)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-09 21:46:36 +08:00
Logicandaias00 862f32e699 [Feature]: enhance logging configuration with asynchronous console appender (#3895)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-12-09 21:15:42 +08:00
DeleiGuo d0f6a04661 [docs] Add new committer blog (#3894) 2025-12-09 09:46:19 +08:00
shownandTomsun28 cb3fad6a2c feat: optimize hertzbeat ai module (#3891)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Signed-off-by: shown <yuluo08290126@gmail.com>
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-08 22:55:44 +08:00
Tomsun28andshown c88c232889 feat: support mcp streamable http protocol and upgrade spring ai version (#3890)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-12-08 22:12:35 +08:00
6b03e4bb5f chore: add .editorconfig for consistent coding style (#3875)
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-12-07 00:02:07 +08:00
7727d63451 [add] Add parameter validators and their corresponding tests (#3888)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-06 19:34:47 +08:00
Duansg 3e567e5be6 [improve]Improve issues related to tag conflicts (#3889) 2025-12-05 22:02:43 +08:00
Tomsun28andaias00 4ae6ba9a3c feat: update add monitors pop selected menus modal (#3878)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-12-04 23:55:10 +08:00
360abcc61d [feature] Using duckdb instead of jpa to store history data (#3883)
Signed-off-by: Logic <zqr10159@126.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 23:51:04 +08:00
P_PeacefulandTomsun28 41e57db032 [bugfix] notice policy lost Gotify receiver type (#3886)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-03 22:44:20 +08:00
DuansgandTomsun28 b62137d185 [improve]Improve performance issues caused by too many metrics (#3879)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-03 22:30:26 +08:00
DuansgandTomsun28 2bd635e1d4 [fix]Fix some systemic issues (#3884)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-03 22:25:02 +08:00
Yang Chen 8e8ab80aa0 Revert "[refractor] Using Spring jdbc to query Greptime log" (#3885) 2025-12-03 12:32:45 +08:00
LogicandYang Chen 7f4c2e0487 [refractor] Using Spring jdbc to query Greptime log (#3880)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: Yang Chen <1597081640@qq.com>
2025-12-03 12:11:08 +08:00
e52e715a53 improvement: optimize log stream for high TPS scenarios (#3876)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-12-03 11:18:49 +08:00
e3e7377d86 [refactor] Refactor Monitor host field to instance and update related logic (#3863)
Signed-off-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-01 21:45:59 +08:00
P_PeacefulandTomsun28 625dbe9624 [bugfix] redis cluster monitor master-slave relationship is inconsistent (#3874)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-11-30 19:39:40 +08:00
Duansgandaias00 42f0a74edf [fix] adding trusted domain verification (#3866)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-11-27 20:38:00 +08:00
Tomsun28 cc9f53d71a chore: update sureness yml (#3870) 2025-11-27 20:10:46 +08:00
DeleiGuo 2f122dfb05 [docs] fix the missing documents references in the sidebar (#3867) 2025-11-26 16:52:43 +08:00
f7b0010180 fix: Remove redundant metrics data send in KafkaCommonDataQueue (#3854)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-24 22:11:49 +08:00
2c0fecaa62 [improve] Added simple exponential backoff strategy (#3860)
Signed-off-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Yang Chen <1597081640@qq.com>
2025-11-21 16:28:21 +08:00
548966906c [bugfix] align tooltip position for mute icon with other icons (#3862)
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-11-20 22:22:56 +08:00
6473546f56 [feat] Add DB2 database monitoring support (#3857)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-20 22:16:52 +08:00
035d63c880 [feature] Support FeiShu/Lark App Alert Notification (#3856)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-11-20 00:14:57 +08:00
Yang ChenandCalvin d56176dfa5 feat: add new docker-compose deployment solution (#3859)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-11-19 23:49:13 +08:00
Yang ChenandCalvin c1815f5107 fix: allow access to /log/** path to resolve unauthorized error on refresh (#3858)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-11-18 01:33:29 +08:00
7ac175e726 [Improve]Unify UI border-radius to 6px for a more professional look (#3853)
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-11-17 23:02:21 +08:00
eb5e843251 [feat] Add webhook authentication support for alert notifications (#3797)
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: 刘俊辉 <liu.junhui3@iwhalecloud.com>
2025-11-17 22:59:35 +08:00
starryCoderandDuansg bc098aa4d2 [bugfix]manager: Avoid NPE when metrics data queue is empty (#3848)
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-14 16:09:49 +08:00
Tomsun28 bcd394b47f [docs] update doc fix wrong manager (#3850)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-08 20:33:42 +08:00
Duansg fd2202fcaf [doc] add new committer blog (#3847) 2025-11-06 21:37:17 +08:00
a7629e317a Modify the issue of incorrect signature in Alibaba's SMS sending (#3788)
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-06 10:38:48 +08:00
DeleiGuoandshown 3a378f8710 [feature] support the security settings for robot signatures in DingTalk (#3841)
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-11-03 11:43:08 +08:00
Tomsun28 cf59907fcd fix the labels limit error (#3842)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-02 20:56:21 +08:00
Tomsun28 624ba8dc8d docs: update readme and doc (#3840) 2025-11-02 12:14:20 +08:00
Tomsun28andaias00 fb4b665c85 refactor ai agent and fix bug, update doc (#3836)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-11-01 21:22:42 +08:00
Calvin 5a9f371e9d [doc] add pmc blog of Calvin, add Duansg as committer and Calvin as PMC (#3838) 2025-11-01 09:38:45 +08:00
Duansg 5c3d1a04de [improve] SLS integrated access supports merged alarm resolution (#3835) 2025-10-31 21:00:34 +08:00
Cyantyandaias00 024b44f477 [ci] replace outdated link addresses in dead link check (#3834)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-10-31 10:16:10 +08:00
Cyanty 35046f8a06 [ci] fix markdown lint ci (#3833) 2025-10-31 09:36:28 +08:00
Duansgandaias00 23c2f85e8a [fix] Optimize the issue where missing tags cause alert statuses to become disordered (#3829)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-10-29 14:24:02 +08:00
Tomsun28 56226c1d52 refactor: split manager to startup module (#3830)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-29 13:49:04 +08:00
Tomsun28 9014f117d3 docs: update contributors page (#3827)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-25 13:53:35 +08:00
Tomsun28andCalvin e073bfee91 refactor: refactor ai feature (#3823)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-10-25 12:39:53 +08:00
Duansg 897d5d4220 [fix]Fixed issue where refresh did not take effect (#3825) 2025-10-24 21:58:22 +08:00
DuansgandTomsun28 4228605a3d [improve] Jexl validation rule enhancements (#3821)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-19 23:08:19 +08:00
DeleiGuo e7131b1881 [docs] fix the issue of avatar reference on the team page (#3820) 2025-10-18 17:59:16 +08:00
DuansgandTomsun28 8a22797bca [fix] Only POST and PUT methods carry payload (#3814)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-14 21:51:50 +08:00
b9e569a1e0 [fix]: fix bugs in frontend for monitoring (#3806)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-13 23:54:11 +08:00
1284b4b46b [feat] Monitoring Center - Added cron expression support for scheduling type (#3777)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-12 00:12:26 +08:00
175c289e66 modify apt sources from http to https. (#3808)
Co-authored-by: liwei <liwei@itg.com.cn>
Co-authored-by: Logic <zqr10159@dromara.org>
2025-10-11 14:37:06 +08:00
Duansg 899e92882d [refactor]Refactoring prometheus label value utf8 support (#3810) 2025-10-11 11:31:09 +08:00
Yang ChenandTomsun28 d516b3a15d improvement: improve log severity text input (#3805)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-08 00:31:31 +08:00
pentium100andTomsun28 7678c3f139 fix some bug in service discovery: (#3804)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-08 00:29:30 +08:00
Tomsun28 3b49c0cb75 chore: use pnpm instead of yarn (#3802) 2025-10-06 21:33:11 +08:00
zhou yong kangandTomsun28 32d784e57e fix: use UTF-8 to solve Chineses bug (#3792)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-05 09:17:33 +08:00
pentium100andTomsun28 d8f8444275 [feature] dns add record type (#3799)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-05 08:58:34 +08:00
Dedy Kurniawan Santoso 41676c1f13 [improve] nitpicking "Dispatch ALl" on en-US.json (#3800)
Signed-off-by: Dedy Kurniawan Santoso <dedy@xendit.co>
2025-10-04 11:08:13 +08:00
Yang ChenandTomsun28 a3def497b4 [fix] remove v174 and add v180 flway script (#3787)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-01 00:20:55 +08:00
381db85149 [OSPP] Modify Dockerfile to support RISC-V. (#3713)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-30 23:22:31 +08:00
会功夫的李白andCalvin d0daa2eec5 [improve] Optimize Gretimedb time-series statistics. (#3776)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-09-25 22:56:13 +08:00
9b5f0f7fce [OSPP] Apache HertzBeat Log Monitoring Capability (#3673)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-25 00:29:00 +08:00
Tom b3ef3d8a6f [docs] update doc and add new contributors (#3783)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-24 14:15:49 +08:00
bd1924b016 [bugfix] fix service discovery host field NullPointerException (#3767)
Co-authored-by: kangli <1363539513@qq.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-09-23 21:23:40 +08:00
1c781d5bc5 [bugfix] webhook url query parameters missing (#3779)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-09-23 09:31:47 +08:00
586aa53e84 [OSPP] Implement a mcp server to run any scripts or commands securely (#3547)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-09-22 19:54:59 +08:00
c5f0389138 [improve] Prometheus streaming parsing optimization enhancements (#3761)
Signed-off-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-21 23:34:06 +08:00
b93613c25e [feat] Support jenkins monitoring (#3774)
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-09-21 15:38:14 +08:00
a8b0c1024d [Improve] Optimise and use the label-selector component for label selection within pages (#3762)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-20 19:16:32 +08:00
Duansg 25a63ccb25 [improve] Improving the operation of threshold rules and the output of expression logs (#3780) 2025-09-20 17:11:19 +08:00
5cdfe545b5 [doc] add japanese i18n in app-idrac.yml (#3766)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-20 00:48:03 +08:00
+16 9be7295084 [fix]bugfix:Prevent Long.parseLong() error when search param is a float (#3483)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Signed-off-by: Cyanty <153884653+Cyanty@users.noreply.github.com>
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: Wenqi Luo <495872803@qq.com>
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Signed-off-by: lynx009 <2030509072@qq.com>
Signed-off-by: Tom <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: DeleiGuo <deleiguo@163.com>
Co-authored-by: Cyanty <153884653+Cyanty@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
Co-authored-by: VampireAchao <achao@apache.org>
Co-authored-by: Wenqi Luo <495872803@qq.com>
Co-authored-by: Yang Chen <1597081640@qq.com>
Co-authored-by: tuzuy <95274591+tuzuy@users.noreply.github.com>
Co-authored-by: tuzy <tuzy.cq@chinaccs.cn>
Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
Co-authored-by: liutianyou <liutianyou@apache.org>
Co-authored-by: carl pinto <carlpinto25@gmail.com>
Co-authored-by: TJxiaobao <2922035952@qq.com>
Co-authored-by: cxhello <49056040+cxhello@users.noreply.github.com>
2025-09-20 00:24:51 +08:00
6905bcc986 [feat] Support monitoring center indicator favorites feature (#3735)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-19 23:53:26 +08:00
Duansg 1050032a13 [improve] SSE exception handling improvements (#3775) 2025-09-19 00:24:49 +08:00
DeleiGuo 9b223dd217 [doc] replace with the new ASF logo (#3770) 2025-09-17 01:16:25 +08:00
Duansg 0a1b5961e9 [feat] Support apollo configuration center monitoring (#3768) 2025-09-15 08:48:52 +08:00
tuzuyandtuzy a8a70b76d7 [feat] Add configuration file for Dell iDRAC monitoring type (#3763)
Co-authored-by: tuzy <tuzy.cq@chinaccs.cn>
2025-09-13 00:08:46 +08:00
+1 05b63903b5 [GSOC] Added all the necessary tools across services (#3722)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: DeleiGuo <deleiguo@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: lynx009 <2030509072@qq.com>
2025-09-10 08:31:05 +08:00
Calvin 7d8b55dfb8 [docs] Update contributors (#3759) 2025-09-09 08:10:40 +08:00
DuansgandTom 72a3e59ca6 [improve] Prometheus streaming parsing optimization (#3752)
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 23:20:23 +08:00
3ab94a751b [Improve] optimize the dashboard page and adapt to the new theme (#3730)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 23:00:12 +08:00
66049f189b [OSPP] Add RISC-V related help document. (#3712)
Signed-off-by: lynx009 <2030509072@qq.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 22:45:21 +08:00
DeleiGuoandTom 69ec3f8751 [bugfix] synchronise AI-related configuration for Docker Compose (#3751)
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 22:42:43 +08:00
feb43c5678 [GSOC] Chat UI, Conversation Management, OpenAI Chat Client Support, More monitoring tools. (#3679)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: DeleiGuo <deleiguo@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-07 22:36:04 +08:00
liutianyou 1fae67282d [doc] update doc for how to release and add release note for 1.7.3 (#3749) 2025-09-06 21:00:04 +08:00
7115 changed files with 800259 additions and 144152 deletions
+200 -10
View File
@@ -7,7 +7,6 @@
"README.md",
"README_CN.md",
"README_JP.md"
"home/src/pages/team/index.jsx"
],
"imageSize": 100,
"commit": true,
@@ -1270,16 +1269,12 @@
]
},
{
"login": "a-little-fool",
"name": "小笨蛋",
"login": "lynx009",
"name": "lynx009",
"avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4",
"profile": "https://github.com/a-little-fool",
"profile": "https://github.com/lynx009",
"contributions": [
"code",
"doc",
"blog",
"bug",
"design"
"doc"
]
},
{
@@ -1442,7 +1437,7 @@
},
{
"login": "Calvin979",
"name": "Naruse",
"name": "Calvin",
"avatar_url": "https://avatars.githubusercontent.com/u/131688897?v=4",
"profile": "https://github.com/Calvin979",
"contributions": [
@@ -2523,6 +2518,201 @@
"contributions": [
"code"
]
},
{
"login": "yy549159265",
"name": "yy549159265",
"avatar_url": "https://avatars.githubusercontent.com/u/40821310?v=4",
"profile": "https://github.com/yy549159265",
"contributions": [
"code",
"test",
"design"
]
},
{
"login": "cto-huhang",
"name": "cto-huhang",
"avatar_url": "https://avatars.githubusercontent.com/u/53338629?v=4",
"profile": "https://github.com/cto-huhang",
"contributions": [
"doc"
]
},
{
"login": "Saramanda9988",
"name": "LunaRain_079",
"avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4",
"profile": "https://github.com/Saramanda9988",
"contributions": [
"doc",
"code"
]
},
{
"login": "delei",
"name": "DeleiGuo",
"avatar_url": "https://avatars.githubusercontent.com/u/17263766?v=4",
"profile": "https://github.com/delei",
"contributions": [
"doc",
"code",
"test",
"bug"
]
},
{
"login": "chingjustwe",
"name": "Rocky, Chi",
"avatar_url": "https://avatars.githubusercontent.com/u/13643747?v=4",
"profile": "https://github.com/chingjustwe",
"contributions": [
"code"
]
},
{
"login": "rowankid",
"name": "Wenqi Luo",
"avatar_url": "https://avatars.githubusercontent.com/u/18652781?v=4",
"profile": "https://github.com/rowankid",
"contributions": [
"bug"
]
},
{
"login": "tuzuy",
"name": "tuzuy",
"avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4",
"profile": "https://github.com/tuzuy",
"contributions": [
"code"
]
},
{
"login": "carlpinto25",
"name": "carl pinto",
"avatar_url": "https://avatars.githubusercontent.com/u/117299909?v=4",
"profile": "https://github.com/carlpinto25",
"contributions": [
"code"
]
},
{
"login": "cxhello",
"name": "cxhello",
"avatar_url": "https://avatars.githubusercontent.com/u/49056040?v=4",
"profile": "https://cxhello.top/",
"contributions": [
"doc"
]
},
{
"login": "jl15988",
"name": "会功夫的李白",
"avatar_url": "https://avatars.githubusercontent.com/u/70638770?v=4",
"profile": "https://github.com/jl15988",
"contributions": [
"code"
]
},
{
"login": "AlbertYang0801",
"name": "Albert.Yang",
"avatar_url": "https://avatars.githubusercontent.com/u/37127008?v=4",
"profile": "https://blog.aytop.cloud/",
"contributions": [
"code"
]
},
{
"login": "mengnankkkk",
"name": "zhou yong kang",
"avatar_url": "https://avatars.githubusercontent.com/u/150590575?v=4",
"profile": "https://blog.tokenlen.top/",
"contributions": [
"code"
]
},
{
"login": "warrobe",
"name": "warrobe",
"avatar_url": "https://avatars.githubusercontent.com/u/89446159?v=4",
"profile": "https://github.com/warrobe",
"contributions": [
"code"
]
},
{
"login": "Jetiaime",
"name": "TeAmo",
"avatar_url": "https://avatars.githubusercontent.com/u/93769000?v=4",
"profile": "https://github.com/Jetiaime",
"contributions": [
"code"
]
},
{
"login": "pentium100",
"name": "pentium100",
"avatar_url": "https://avatars.githubusercontent.com/u/27917?v=4",
"profile": "https://github.com/pentium100",
"contributions": [
"code"
]
},
{
"login": "dedyks",
"name": "Dedy Kurniawan Santoso",
"avatar_url": "https://avatars.githubusercontent.com/u/23741665?v=4",
"profile": "https://github.com/dedyks",
"contributions": [
"code"
]
},
{
"login": "KOYR",
"name": "KOYR",
"avatar_url": "https://avatars.githubusercontent.com/u/53216619?v=4",
"profile": "https://github.com/KOYR",
"contributions": [
"code"
]
},
{
"login": "Lathika226",
"name": "LathikaBaddam",
"avatar_url": "https://avatars.githubusercontent.com/u/178710568?v=4",
"profile": "https://github.com/Lathika226",
"contributions": [
"doc"
]
},
{
"login": "Sahil-Shadwal",
"name": "Sahil Shadwal",
"avatar_url": "https://avatars.githubusercontent.com/u/119167601?v=4",
"profile": "https://shadwal.space/",
"contributions": [
"code"
]
},
{
"login": "BhanuNidumolu",
"name": "N.Bhanu Prasad",
"avatar_url": "https://avatars.githubusercontent.com/u/180380413?v=4",
"profile": "https://github.com/BhanuNidumolu",
"contributions": [
"code"
]
},
{
"login": "Prakash1185",
"name": "Prakash Kumar",
"avatar_url": "https://avatars.githubusercontent.com/u/183058331?v=4",
"profile": "https://prakashh-portfolio.vercel.app/",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+6 -6
View File
@@ -16,11 +16,15 @@
#
github:
description: Real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
description: An AI-powered next-generation open source real-time observability system.
homepage: https://hertzbeat.apache.org/
labels:
- monitoring
- ai
- agent
- llm
- logs
- monitor
- monitoring
- notifications
- alerting
- self-hosted
@@ -30,15 +34,11 @@ github:
- metrics
- observability
- uptime
- uptime-monitoring
- status
- status-page
- devops
- server
- linux
- database
- mysql
- cloud
enabled_merge_buttons:
squash: true
merge: false
-1
View File
@@ -15,7 +15,6 @@
"customizations": {
"vscode": {
"settings": {},
// same extensions as Gitpod, should match /.gitpod.yml
"extensions": [
"vscjava.vscode-java-pack",
"editorconfig.editorconfig",
+53
View File
@@ -0,0 +1,53 @@
# 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.
#
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
# The line max length is 120, match checkstyle rules.
max_line_length = 120
insert_final_newline = true
trim_trailing_whitespace = true
[*.json]
tab_width = 2
indent_size = 2
[*.{yml,yaml}]
tab_width = 2
indent_size = 2
[*.xml]
ij_xml_attribute_wrap = off
ij_xml_text_wrap = off
ij_xml_keep_blank_lines = 1
[pom.xml]
indent_size = 2
[*.java]
indent_size = 4
tab_width = 4
[*.{js,jsx,mjs,cjs,ts,tsx}]
indent_size = 2
tab_width = 2
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
# 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.
set -eu
common_hook="$(git rev-parse --git-common-dir)/hooks/post-commit"
if [ -x "$common_hook" ]; then
exec "$common_hook" "$@"
fi
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# 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.
set -eu
repo_root=$(git rev-parse --show-toplevel)
cd "$repo_root/web-app"
node scripts/check-toolchain.mjs
exec pnpm format:staged
+10 -10
View File
@@ -90,24 +90,24 @@ e2e:
backend:
- changed-files:
- any-glob-to-any-file:
- "alerter/**/*"
- "collector/**/*"
- "common/**/*"
- "grafana/**/*"
- "manager/**/*"
- "push/**/*"
- "remoting/**/*"
- "warehouse/**/*"
- "hertzbeat-alerter/**/*"
- "hertzbeat-collector/**/*"
- "hertzbeat-common/**/*"
- "hertzbeat-grafana/**/*"
- "hertzbeat-manager/**/*"
- "hertzbeat-push/**/*"
- "hertzbeat-remoting/**/*"
- "hertzbeat-warehouse/**/*"
collector:
- changed-files:
- any-glob-to-any-file:
- "collector/**/*"
- "hertzbeat-collector/**/*"
monitoring-template:
- changed-files:
- any-glob-to-any-file:
- "manager/src/main/resources/define/**/*"
- "hertzbeat-manager/src/main/resources/define/**/*"
HIP:
- changed-files:
+49 -9
View File
@@ -20,34 +20,75 @@
name: Backend CI
permissions:
contents: read
concurrency:
group: backend-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [ master, dev, action* ]
branches: [ master, dev, action*, '2.*' ]
paths-ignore:
- '**.md'
- 'home/**'
- 'web-app/**'
- 'script/**'
- 'material/**'
pull_request:
branches: [ master, dev ]
branches: [ master, dev, '2.*' ]
paths-ignore:
- '**.md'
- 'home/**'
- 'web-app/**'
- 'script/**'
- 'material/**'
jobs:
backend-build:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: ./script/ci/github-actions/setup-deps
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Enable pnpm
run: |
corepack enable
corepack prepare pnpm@10.9.0 --activate
- name: Resolve pnpm store
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ hashFiles('web-app/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- name: Verify server release layout
run: |
./script/ci/verify-server-release-layout.sh
python3 -m unittest script.ci.test_verify_server_release_package
- name: Build frontend release assets
working-directory: web-app
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Build with Maven
run: mvnd clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
- name: Verify server release package
run: |
release_version=$(sed -n 's:.*<hzb.version>\([^<][^<]*\)</hzb.version>.*:\1:p' pom.xml | head -1)
server_archive="dist/apache-hertzbeat-${release_version}-bin.tar.gz"
test -f "$server_archive"
python3 script/ci/verify-server-release-package.py "$server_archive"
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
@@ -66,11 +107,11 @@ jobs:
run: |
sudo curl -L https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
sudo chmod u+x /usr/local/bin/docker-compose
cd e2e
sudo docker-compose version
sudo docker-compose up --exit-code-from testing --remove-orphans
# upload application logs
- name: Upload logs & API test reports
uses: actions/upload-artifact@v4
@@ -80,4 +121,3 @@ jobs:
path: |
e2e/logs/
e2e/report/
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
python-version: '3.8'
- name: Check Markdown
uses: DavidAnson/markdownlint-cli2-action@v16
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e
with:
globs: './home/**/*.md'
+88
View File
@@ -0,0 +1,88 @@
# 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.
name: DOC Deploy
on:
# Trigger on push to master branch when home directory files are changed
push:
branches: [ master ]
paths:
- 'home/**'
# Allow manual workflow trigger
workflow_dispatch:
# Set permissions for deployment
permissions:
contents: write
jobs:
deploy:
name: Deploy Documentation to asf-site Branch
runs-on: ubuntu-latest
steps:
# Checkout the repository code
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup Node.js environment
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# Setup pnpm
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
# Install dependencies in home directory
- name: Install Dependencies
working-directory: home
run: pnpm install --frozen-lockfile
# Build the documentation site
- name: Build Documentation
working-directory: home
run: pnpm run build
env:
NODE_OPTIONS: --max-old-space-size=4096
# Deploy to asf-site branch using peaceiris/actions-gh-pages
- name: Deploy to asf-site Branch
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./home/build
publish_branch: asf-site
force_orphan: true
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'
commit_message: 'Deploy documentation to asf-site branch'
# Output deployment status
- name: Deployment Summary
run: |
echo "Documentation deployed successfully!"
echo "Deployment branch: asf-site"
echo "Source branch: ${{ github.ref_name }}"
echo "Commit SHA: ${{ github.sha }}"
+35 -10
View File
@@ -15,29 +15,54 @@
# specific language governing permissions and limitations
# under the License.
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Frontend CI
name: Frontend CI
permissions:
contents: read
concurrency:
group: frontend-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [ master, dev, action* ]
branches: [ master, dev, 'action*', 'codex/**', 'feature/**', 'release/**', '2.*' ]
paths:
- 'web-app/**'
- '.github/workflows/frontend-build-test.yml'
pull_request:
branches: [ master, dev ]
branches: [ master, dev, 'codex/**', 'feature/**', 'release/**', '2.*' ]
paths:
- 'web-app/**'
- '.github/workflows/frontend-build-test.yml'
jobs:
build:
verify:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Yarn Install
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Enable pnpm
run: |
corepack enable
corepack prepare pnpm@10.9.0 --activate
- name: Resolve pnpm store
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ hashFiles('web-app/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- name: Install
working-directory: web-app
run: yarn
- name: EsLint Test
run: pnpm install --frozen-lockfile
- name: Verify
working-directory: web-app
run: yarn lint:ts
run: pnpm verify
@@ -0,0 +1,233 @@
# 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.
name: Hybrid Collector Release Gate
on:
pull_request:
paths:
- '.github/workflows/hybrid-collector-release.yml'
- '.github/workflows/nightly-build.yml'
- 'pom.xml'
- 'hertzbeat-collector/**'
- 'hertzbeat-common-core/**'
- 'hertzbeat-manager/**'
- 'hertzbeat-observability/**'
# Negative paths: run the module-boundary contract if a legacy module reappears.
- 'hertzbeat-log/**'
- 'hertzbeat-otel/**'
- 'hertzbeat-otel-runtime/**'
- 'hertzbeat-startup/**'
- 'script/assembly/collector/**'
- 'script/ci/*hybrid-collector*'
- 'script/ci/test_observability_module_boundary.py'
- 'script/ci/*otel-runtime*'
- 'script/docker/collector/**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: hybrid-collector-${{ github.ref }}
cancel-in-progress: true
jobs:
contract:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: hertzbeat-otel-runtime/go.mod
cache-dependency-path: hertzbeat-otel-runtime/go.mod
- uses: graalvm/setup-graalvm@v1
with:
java-version: 25
distribution: graalvm
github-token: ${{ secrets.GITHUB_TOKEN }}
cache: maven
- name: Verify source and dependency contracts
run: |
python3 script/ci/test_observability_module_boundary.py -v
python3 script/ci/test_verify_hybrid_collector_release_content.py -v
python3 script/ci/test_verify_hybrid_collector_jvm_package.py -v
python3 script/ci/test_verify_otel_runtime_sbom_platform.py -v
python3 script/ci/test_prepare_hybrid_collector_native_container_context.py -v
python3 script/ci/test_hybrid_collector_systemd_install.py -v
mkdir -p target/release-policy
git archive --format=tar.gz --output target/release-policy/hertzbeat-source.tar.gz HEAD
python3 script/ci/verify-hybrid-collector-release-content.py \
--source target/release-policy/hertzbeat-source.tar.gz
./script/ci/verify-hybrid-collector-release-layout.sh
./script/ci/verify-otel-runtime-layout.sh
./script/ci/verify-hybrid-collector-native-layout.sh
make -C hertzbeat-otel-runtime validate
make -C hertzbeat-otel-runtime license-check
- name: Verify Go binary reproducibility
run: ./script/ci/verify-otel-runtime-reproducibility.sh
- name: Verify Java runtime and Spring AOT
run: |
./script/ci/verify-hybrid-collector-java.sh
make -C hertzbeat-otel-runtime release-assets
./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -am \
-Pruntime -DskipTests package
set -- dist/apache-hertzbeat-collector-*-bin.tar.gz
test "$#" -eq 1 && test -f "$1"
sh ./script/ci/verify-hybrid-collector-jvm-package.sh "$1" generic
set -- dist/apache-hertzbeat-collector-*-bin-linux_amd64.tar.gz
test "$#" -eq 1 && test -f "$1"
sh ./script/ci/verify-hybrid-collector-jvm-package.sh "$1" linux-amd64
source-policy:
strategy:
fail-fast: false
matrix:
runner: [ubuntu-24.04, macos-15, windows-2025]
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: graalvm/setup-graalvm@v1
with:
java-version: 25
distribution: graalvm
github-token: ${{ secrets.GITHUB_TOKEN }}
cache: maven
- name: Verify cross-platform source policy
shell: bash
run: |
./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -am \
-Dtest='ManagedOtelRuntimeConfigTest,OtelRuntimeConfigRendererTest,OtelRuntimeSourcePolicyTest' \
test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false
native-release:
if: github.event_name == 'workflow_dispatch'
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
platform: linux-amd64
- runner: ubuntu-24.04-arm
platform: linux-arm64
- runner: macos-15-intel
platform: macos-amd64
- runner: macos-15
platform: macos-arm64
- runner: windows-2025
platform: windows-amd64
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: hertzbeat-otel-runtime/go.mod
cache-dependency-path: hertzbeat-otel-runtime/go.mod
- uses: graalvm/setup-graalvm@v1
with:
java-version: 25
distribution: graalvm
github-token: ${{ secrets.GITHUB_TOKEN }}
cache: maven
- name: Install make on Windows
if: runner.os == 'Windows'
shell: pwsh
run: choco install make --no-progress -y
- name: Build release assets
shell: bash
run: make -C hertzbeat-otel-runtime release-assets
- name: Build native Collector
shell: bash
run: ./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -am -Pnative -DskipTests package
- name: Verify native package
shell: bash
run: |
archive=$(find dist -maxdepth 1 -type f -name "*-${{ matrix.platform }}-bin.*" -print -quit)
test -n "$archive"
./script/ci/verify-hybrid-collector-native-package.sh "$archive" "${{ matrix.platform }}"
- uses: actions/upload-artifact@v4
with:
name: hybrid-collector-package-${{ matrix.platform }}
path: dist/*-${{ matrix.platform }}-bin.*
if-no-files-found: error
native-container:
if: github.event_name == 'workflow_dispatch'
needs: native-release
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
pattern: hybrid-collector-package-linux-*
path: dist
merge-multiple: true
- name: Prepare verified native container inputs
run: ./script/ci/prepare-hybrid-collector-native-container-context.sh dist target/native-container-context
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Build runnable amd64 native image
uses: docker/build-push-action@v6
with:
context: target/native-container-context
file: script/docker/collector/Dockerfile.native
platforms: linux/amd64
load: true
tags: apache/hertzbeat-collector:native-test
- name: Verify native image runtime
run: ./script/ci/verify-hybrid-collector-native-image.sh apache/hertzbeat-collector:native-test linux/amd64
- name: Build multi-platform native image
uses: docker/build-push-action@v6
with:
context: target/native-container-context
file: script/docker/collector/Dockerfile.native
platforms: linux/amd64,linux/arm64
push: false
outputs: type=oci,dest=/tmp/hybrid-collector.oci.tar
tags: apache/hertzbeat-collector:native-test
- name: Verify multi-platform native image layers
run: |
python3 script/ci/verify-hybrid-collector-release-content.py \
--container-image /tmp/hybrid-collector.oci.tar
- uses: actions/upload-artifact@v4
with:
name: hybrid-collector-native-oci
path: /tmp/hybrid-collector.oci.tar
if-no-files-found: error
native-checksums:
if: github.event_name == 'workflow_dispatch'
needs: native-release
runs-on: ubuntu-24.04
steps:
- uses: actions/download-artifact@v4
with:
pattern: hybrid-collector-package-*
path: release
merge-multiple: true
- name: Generate archive checksums
working-directory: release
run: find . -maxdepth 1 -type f -print0 | sort -z | xargs -0 sha512sum > SHA512SUMS
- uses: actions/upload-artifact@v4
with:
name: hybrid-collector-native-checksums
path: release/SHA512SUMS
if-no-files-found: error
+97
View File
@@ -0,0 +1,97 @@
# 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.
name: MCP Bash Server CI
on:
push:
paths:
- 'mcp-servers/mcp-bash-server/**'
- '.github/workflows/mcp-bashserver-test.yml'
pull_request:
paths:
- 'mcp-servers/mcp-bash-server/**'
- '.github/workflows/mcp-bashserver-test.yml'
env:
CARGO_TERM_COLOR: always
RUST_VERSION: 1.88.0
jobs:
test:
name: Test MCP Bash Server
runs-on: ubuntu-latest
strategy:
matrix:
profile: [dev, release]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
components: rustfmt, clippy
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-registry-
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target/
key: ${{ runner.os }}-cargo-build-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-build-${{ matrix.profile }}-
${{ runner.os }}-cargo-build-
- name: Check code format
run: cargo fmt --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml --check
- name: Run cargo check
run: |
if [ "${{ matrix.profile }}" = "release" ]; then
cargo check --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
else
cargo check --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
fi
- name: Run clippy
run: |
if [ "${{ matrix.profile }}" = "release" ]; then
cargo clippy --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
else
cargo clippy --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
fi
- name: Run tests
run: |
if [ "${{ matrix.profile }}" = "release" ]; then
cargo test --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
else
cargo test --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
fi
+59 -8
View File
@@ -17,6 +17,13 @@
name: Nightly CI
permissions:
contents: read
concurrency:
group: nightly-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
schedule:
# trigger at 00:00 everyday
@@ -27,23 +34,56 @@ on:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 180
if: ${{ github.repository == 'apache/hertzbeat' }}
steps:
- uses: actions/checkout@v4
- uses: ./script/ci/github-actions/setup-deps
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: actions/setup-go@v6
with:
go-version-file: hertzbeat-otel-runtime/go.mod
cache-dependency-path: hertzbeat-otel-runtime/go.mod
- uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 25
- name: Build the Frontend
run: |
corepack enable
corepack prepare pnpm@10.9.0 --activate
cd web-app
yarn install
yarn package
pnpm install --frozen-lockfile
pnpm build
- name: Build and verify managed Collector runtimes
run: |
make -C hertzbeat-otel-runtime release-assets
./script/ci/verify-otel-runtime-package-layout.sh
- name: Build the Backend
run: |
mvn clean install
mvn clean package -Prelease -DskipTests
cd hertzbeat-collector
mvn clean package -Pcluster -DskipTests
./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -am \
-Pruntime -DskipTests package
- name: Verify Linux Collector release content
run: |
set -- dist/apache-hertzbeat-collector-*-bin-linux_amd64.tar.gz
if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
echo "expected exactly one generated linux-amd64 Hybrid Collector archive" >&2
exit 1
fi
./script/ci/verify-hybrid-collector-jvm-package.sh "$1" linux-amd64
set -- dist/apache-hertzbeat-collector-*-bin-linux_arm64.tar.gz
if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
echo "expected exactly one generated linux-arm64 Hybrid Collector archive" >&2
exit 1
fi
./script/ci/verify-hybrid-collector-jvm-package.sh "$1" linux-arm64
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
@@ -62,11 +102,22 @@ jobs:
push: true
tags: apache/hertzbeat:nightly
- name: Build and Push Collector
- name: Build Collector image for verification
uses: docker/build-push-action@v6
with:
context: ./dist
file: ./script/docker/collector/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: apache/hertzbeat-collector:nightly
outputs: type=oci,dest=/tmp/hybrid-collector-nightly.oci.tar
- name: Verify final Collector image content
run: python3 script/ci/verify-hybrid-collector-release-content.py --container-image /tmp/hybrid-collector-nightly.oci.tar
- name: Push verified Collector image
run: |
sudo apt-get update
sudo apt-get install -y skopeo
skopeo copy --all --authfile "$HOME/.docker/config.json" \
oci-archive:/tmp/hybrid-collector-nightly.oci.tar \
docker://apache/hertzbeat-collector:nightly
+16
View File
@@ -46,6 +46,18 @@ nbdist/
node_modules
.docusaurus
# codex / browser / screenshot noise
.codex-artifacts/
.playwright-mcp/
progress.md
test-results/
playwright-report/
reports/
screenshots/
# local temp runtime data
.tmp/
# h2 data
*.mv.db
*.trace.db
@@ -56,3 +68,7 @@ application-mysql.yml
application-pg.yml
/package.json
/yarn.lock
.omx/
# legacy frontend build output
/web-app/dist/
+2 -2
View File
@@ -3,5 +3,5 @@ FROM gitpod/workspace-full
USER gitpod
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \
sdk install java 17.0.3-ms && \
sdk default java 17.0.3-ms"
sdk install java 21.0.2-tem && \
sdk default java 21.0.2-tem"
+3 -3
View File
@@ -4,7 +4,7 @@ image:
tasks:
- name: Run backend
before: cd hertzbeat-manager
before: cd hertzbeat-startup
command: |
gp sync-await setup-backend
mvn spring-boot:run
@@ -13,7 +13,7 @@ tasks:
before: cd web-app
command: |
gp sync-await setup-frontend
yarn start --public-host "`gp url 4200`"
pnpm start --public-host "`gp url 4200`"
openMode: split-right
- name: Setup backend
@@ -25,7 +25,7 @@ tasks:
- name: Setup frontend
init: |
cd web-app && yarn install
cd web-app && pnpm install
command: |
gp sync-done setup-frontend
exit 0
+2
View File
@@ -39,6 +39,7 @@ header:
- '**/*.js'
- '**/*.less'
- '**/*.txt'
- '**/*.st'
- '**/target/**'
- '.gitattributes'
- '**/.gitignore'
@@ -66,6 +67,7 @@ header:
- '.gitpod.yml'
- 'karma.conf.js'
- 'proxy.conf.js'
- 'pnpm-lock.yaml'
- '.helmignore'
- 'script/helm/**'
- 'web-app/src/app/core/**'
+1 -1
View File
@@ -31,7 +31,7 @@
"MD025": true,
"MD029": true,
"MD033": {
"allowed_elements": ["table", "tr", "td", "a", "img", "sub", "b", "br", "img", "tbody", "mark", "font"]
"allowed_elements": ["table", "tr", "td", "a", "img", "sub", "b", "br", "img", "tbody", "mark", "font", "Tabs", "TabItem"]
},
"MD036": false,
"MD040": true,
+38 -48
View File
@@ -20,43 +20,39 @@
- 💬**Suggestion**: You can also make some suggestions to the project or community to promote the healthy development of the community;
More see [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
More see [Contribution Types](https://allcontributors.org/emoji-key)
Even small corrections to typos are very welcome :)
### Getting HertzBeat up and running
> To get HertzBeat code running on your development tools, and able to debug with breakpoints.
> This is a front-end and back-end separation project. To start the local code, the back-end [manager](manager) and the front-end [web-app](web-app) must be started separately.
> This is a front-end and back-end separation project. To start the local code, the back-end [startup](hertzbeat-startup) and the front-end [web-app](web-app) must be started separately.
#### Backend start
1. Requires `maven3+`, `java17` and `lombok` environments
1. Requires `maven3+`, Java 25 and `lombok` environments
2. (Optional) Modify the configuration file: `manager/src/main/resources/application.yml`
2. (Optional) Modify the configuration file: `hertzbeat-startup/src/main/resources/application.yml`
3. Execute under the project root directory: `mvn clean install -DskipTests`
4. Add VM Options: `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
5. Start `springboot manager` service: `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
5. Start `springboot startup` service: `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
#### Frontend start
1. Need `Node Yarn` Environment, Make sure `Node.js >= 18`
1. Need `Node.js 22` and `pnpm 10`
2. Cd to the `web-app` directory: `cd web-app`
3. Install yarn if not existed `npm install -g yarn`
3. Install dependencies: `corepack pnpm@10.9.0 install --frozen-lockfile`
4. Install Dependencies: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
4. After the local backend is started, start the local frontend in the `web-app` directory: `corepack pnpm@10.9.0 dev`
5. Install angular-cli globally: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
6. After the local backend is started, start the local frontend in the web-app directory: `ng serve --open`
7. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
5. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
### Find tasks
@@ -91,6 +87,16 @@ Of course, if you have a good idea, you can also propose it directly on GitHub D
Please note that the title of the PR needs to conform to our spec, and write the necessary description in the PR to facilitate code review by Committers and other contributors.
### AI-assisted contributions
AI-assisted code is welcome when it is reviewed and owned by a human contributor. Please follow [AI-Assisted Development Guidelines](AGENTS.md):
1. Treat AI output as a draft and keep each PR narrowly scoped.
2. Do not include proprietary, private, license-incompatible, or secret material from AI prompts or outputs.
3. Do not invent product behavior, API responses, topology data, metrics, health states, screenshots, or release claims.
4. Write or update the smallest useful test/contract first for risky changes, then include the validation commands and results in the PR description.
5. If AI helped, add a short PR note: `AI assistance`, `Human validation`, and `Risk notes`.
### Wait for the code to be merged
After submitting the PR, the Committee or the community's friends will review the code you submitted (Code Review), and will propose some modification suggestions or conduct some discussions. Please pay attention to your PR in time.
@@ -133,17 +139,6 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
## 🥐 Architecture
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** Provide monitoring management, system management basic services.
> Provides monitoring management, monitoring configuration management, system user management, etc.
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** Provide metrics data collection services.
> Use common protocols to remotely collect and obtain peer-to-peer metrics data.
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** Provide monitoring data warehousing services.
> Metrics data management, data query, calculation and statistics.
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** Provide alert service.
> Alarm calculation trigger, monitoring status linkage, alarm configuration, and alarm notification.
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** Provide web ui.
> Angular Web UI.
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
<br>
@@ -174,43 +169,39 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
- 💬**建议**:也可以对项目或者社区提出一些建议,促进社区的良性发展;
更多贡献方式参见 [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
更多贡献方式参见 [Contribution Types](https://allcontributors.org/emoji-key)
即便是小到错别字的修正我们也都非常欢迎 :)
### 让 HertzBeat 运行起来
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
> 此为前后端分离项目,本地代码启动需将后端[manager](manager)和前端[web-app](web-app)分别启动生效。
> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-app](web-app)分别启动生效。
#### 后端启动
1. 需要 `maven3+`, `java17` 和 `lombok` 环境
1. 需要 `maven3+`、Java 25 和 `lombok` 环境
2. (可选)修改配置文件配置信息-`manager/src/main/resources/application.yml`
2. (可选)修改配置文件配置信息-`hertzbeat-startup/src/main/resources/application.yml`
3. 在项目根目录运行编译: `mvn clean install -DskipTests`
4. 在 `jvm` 加入参数 `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
5. 启动`springboot manager`服务 `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
5. 启动`springboot startup`服务 `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
#### 前端启动
1. 需要 `nodejs yarn` 环境, Make sure `Node.js >= 18`
1. 需要 `Node.js 22` 和 `pnpm 10`
2. 进入 `web-app` 目录: `cd web-app`
3. 安装yarn: `npm install -g yarn`
3. 安装依赖: `corepack pnpm@10.9.0 install --frozen-lockfile`
4. 在前端工程目录 `web-app` 下执行: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
4. 待本地后端启动后,在 `web-app` 目录下启动本地前端: `corepack pnpm@10.9.0 dev`
5. 全局安装 `angular-cli`: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
6. 待本地后端启动后,在web-app目录下启动本地前端 `ng serve --open`
7. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
5. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
### 寻找任务
@@ -244,6 +235,16 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
请注意 PR 的标题需要符合我们的规范,并且在 PR 中写上必要的说明,来方便 Committer 和其他贡献者进行代码审查。
### AI 辅助贡献
欢迎使用 AI 辅助写代码,但 PR 必须由人类贡献者负责、审查和验证。请遵循 [AI-Assisted Development Guidelines](AGENTS.md)
1. 把 AI 输出当作草稿,每个 PR 保持清晰、收敛的范围。
2. 不要提交来自 AI 提示词或输出中的私有、专有、许可证不兼容内容,也不要包含密钥或凭据。
3. 不要编造产品行为、API 响应、拓扑数据、指标、健康状态、截图或发布结论。
4. 对高风险或用户可见改动,先写或更新最小测试/合约,再在 PR 描述中写明验证命令和结果。
5. 如果使用了 AI,请在 PR 描述中简短说明:`AI assistance`、`Human validation`、`Risk notes`。
### 等待PR代码被合并
在提交了 PR 后,Committer 或者社区的小伙伴们会对您提交的代码进行审查(Code Review),会提出一些修改建议,或者是进行一些讨论,请及时关注您的PR。
@@ -286,15 +287,4 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
### 模块
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** 提供监控管理,系统管理基础服务
> 提供对监控的管理,监控应用配置的管理,系统用户租户后台管理等。
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** 提供监控数据采集服务
> 使用通用协议远程采集获取对端指标数据。
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** 提供监控数据仓储服务
> 采集指标结果数据管理,数据落盘,查询,计算统计。
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** 提供告警服务
> 告警计算触发,任务状态联动,告警配置,告警通知。
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** 提供可视化控制台页面
> 监控告警系统可视化控制台前端
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
+49 -25
View File
@@ -9,8 +9,6 @@
<b><a href="README.md">English</a></b> | <a href="README_CN.md">中文</a> | <a href="README_JP.md">日本語</a>
</p>
> A real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024)
@@ -21,6 +19,7 @@
[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCri75zfWX0GHqJFPENEbLow?logo=youtube&label=YouTube%20Channel)](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/apache/hertzbeat)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/apache/hertzbeat)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/apache/hertzbeat)
**Home: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
**Email: <a href="mailto:dev-subscribe@hertzbeat.apache.org">Mail to `dev-subscribe@hertzbeat.apache.org`</a> to subscribe mailing lists**
@@ -28,28 +27,23 @@
## 🎡 <font color="green">Introduction</font>
[Apache HertzBeat™](https://github.com/apache/hertzbeat) is an easy-to-use, open source, real-time observability system with agentless, high performance cluster, prometheus-compatible, offers powerful custom monitoring and status page building capabilities.
[Apache HertzBeat™](https://github.com/apache/hertzbeat) is an AI-powered next-generation open source real-time observability system. Unified metrics and logs collection, centralized alerting distribution, intelligent management and analysis. No Agent required, high performance cluster, provides powerful custom monitoring and status page building capabilities.
### Features
* Combines **monitoring, alarm, and notification** features into one platform, and supports monitoring for web service, program, database, cache, os, webserver, middleware, bigdata, cloud-native, network, custom and more.
* Easy to use and agentless, web-based and with one-click monitoring and alerting, zero learning curve.
* Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
* Compatible with the `Prometheus` ecosystem and more, can monitoring what `Prometheus` can monitoring with few clicks on webui.
* High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
* Provides flexible alarm threshold rules and timely notifications delivered via `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`.
* Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
- Integrates **collection + analysis + alerting + notification** into one platform, with new AI-powered interactions and features under HertzBeat AI, and built-in MCP Server capabilities.
- Unified metrics platform, agentless, Prometheus-compatible, supports application services, programs, databases, caches, operating systems, big data, middleware, web servers, cloud-native, networks, custom monitoring and more.
- Unified logging platform, seamlessly integrates multiple log sources through OTLP protocol for reporting.
- Unified alerting platform, integrates internal alerts with various external alert sources, unified alert processing and analysis, flexible real-time and periodic threshold rules, grouping convergence, silence, suppression, etc.
- Unified message distribution, alerts processed by the alerting platform are distributed via `Email` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` and other methods.
- Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
- High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
- Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
> HertzBeat's powerful customization, multi-type support, high performance, easy expansion, and low coupling, aims to help users quickly build their own monitoring system.
> HertzBeat's unified platform, AI intelligence, powerful customization, multi-type support, high performance, and easy expansion, aims to help users quickly and conveniently achieve observability requirements.
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 Architecture
@@ -58,8 +52,8 @@
## ⛄ Supported
> We define all monitoring collection types such as `mysql`, `jvm`, and `k8s` as `YML` monitoring templates, allowing users to import them to support corresponding types of monitoring.
> Welcome everyone to contribute your customized general monitoring type YML template during use.
> We define all metrics collection types such as `mysql`, `jvm`, and `k8s` as `YML` templates, allowing users to import them to support corresponding types of monitoring.
> Welcome everyone to contribute your customized general metrics type YML template during use.
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
@@ -104,6 +98,9 @@
## 🐕 Quick Start
- Community alpha preview users can start from [docs/alpha-preview.md](docs/alpha-preview.md) for the current
Next.js frontend, entity-centered observability, local smoke, and known alpha limitations.
- If you wish to deploy HertzBeat locally, please refer to the following Deployment Documentation for instructions.
### 🍞 Install HertzBeat
@@ -161,9 +158,9 @@ Detailed config refer to [Install HertzBeat via Package](https://hertzbeat.apach
##### 3Start via source code
1. Local source code debugging needs to start the back-end project `manager` and the front-end project `web-app`.
2. Backendneed `maven3+`, `java17`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `manager` service.
3. Webneed `nodejs npm angular-cli` environment, Run `ng serve --open` in `web-app` directory after backend startup.
1. Local source code debugging needs to start the back-end project `hertzbeat-startup` and the front-end project `web-app`.
2. Backend: need `maven3+`, Java 25, and `lombok`; add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `hertzbeat-startup` service.
3. Web: need `Node.js 22` and `pnpm 10`, run `corepack pnpm@10.9.0 install --frozen-lockfile && corepack pnpm@10.9.0 dev` in `web-app` after backend startup.
4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat`
Detailed steps refer to [CONTRIBUTING](CONTRIBUTING.md)
@@ -361,7 +358,7 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
@@ -385,7 +382,7 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
@@ -529,6 +526,33 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -565,7 +589,7 @@ WeChat Public : Search ID `usthecom`.
HertzBeat is built on so many great open source projects, thanks to them!
- `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...`
- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...`
- `TypeScript Next.js React Tailwind CSS Node.js Npm ECharts G6 MonacoEditor Docusaurus ...`
## Landscape
+44 -24
View File
@@ -9,8 +9,6 @@
<a href="README.md">English</a> | <b><a href="README_CN.md">中文</a></b> | <a href="README_JP.md">日本語</a>
</p>
> 实时观测系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024)
@@ -28,29 +26,24 @@
## 🎡 <font color="green">介绍</font>
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是 AI 驱动的下一代开源实时观测系统。指标日志统一收集,告警一站分发,智能管控分析。无需 Agent,高性能集群,提供强大的自定义监控和状态页构建能力。
### 特点
- **监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等监控阈值告警通知一步到位
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,无需学习成本。
- `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模板 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需简单配置即可快速适配一款 `K8s``Docker` 等新的监控类型吗?
- 兼容 `Prometheus` 的系统生态并且更多,只需页面操作就可以监控 `Prometheus` 所能监控的
-```采集+分析+告警+通知```为一体,HertzBeat AI 驱动下的新交互与功能,也内置 MCP Server 对外能力
- 统一的指标平台,无需 Agent,兼容 Prometheus,支持应用服务,程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等。
- 统一的日志平台,通过 OTLP 协议多日志源无缝对接上报。
- 统一的告警平台,内部告警与外部多种告警源集成接入,统一告警处理分析,灵活的实时与周期阈值规则,分组收敛,静默,抑制等
- 统一的消息分发,告警平台处理后通过 `邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式分发通知。
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需配置模板 `YML` 就能自定义采集指标。您相信只需简单配置即可快速适配一款 `K8s``Docker` 等新的监控类型吗?
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
- 灵活的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
> `HertzBeat`的强大自定义,多类型支持,高性能,易扩展,低耦合,希望能帮助用户快速搭建自有监控系统
> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 模块
@@ -59,8 +52,8 @@
## ⛄ 已支持
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模板,用户可以导入这些模板来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模板。
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 模板,用户可以导入这些模板来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用指标模板。
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
@@ -98,7 +91,7 @@
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
- 和更多自定义监控模板。
- 和更多自定义指标模板。
- 通知支持 `Discord` `Slack` `Telegram` `邮件` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱`
## 🐕 快速开始
@@ -159,9 +152,9 @@
##### 方式三:本地代码启动
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `manager` 和前端工程 `web-app`
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `manager` 服务即可。
3. 前端:需要 `nodejs npm angular-cli`环境,待本地后端启动后,在 `web-app` 目录下启动 `ng serve --open`
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `hertzbeat-startup` 和前端工程 `web-app`
2. 后端:需要 `maven3+`、Java 25 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `hertzbeat-startup` 服务即可。
3. 前端:需要 `Node.js 22` 和 `pnpm 10` 环境,待本地后端启动后,在 `web-app` 目录下执行 `corepack pnpm@10.9.0 install --frozen-lockfile && corepack pnpm@10.9.0 dev`
4. 浏览器访问 `http://localhost:4200` 即可开始,默认账号密码 `admin/hertzbeat`
详细步骤参考 [参与贡献之本地代码启动](CONTRIBUTING.md)
@@ -359,7 +352,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
@@ -383,7 +376,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
@@ -527,6 +520,33 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -564,7 +584,7 @@ Thanks these wonderful people, welcome to join us:
HertzBeat is built on so many great open source projects, thanks to them!
- `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...`
- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...`
- `TypeScript Next.js React Tailwind CSS Node.js Npm ECharts G6 MonacoEditor Docusaurus ...`
## Landscape
+55 -36
View File
@@ -7,9 +7,7 @@
<p align="center">
<b>Readme</b>:
<a href="README.md">English</a> | <a href="README_CN.md">中文</a> | <b><a href="README_JP.md">日本語</a></b>
</p>
> オープンソースのリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き。
</p>
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
@@ -22,44 +20,38 @@
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/apache/hertzbeat)
**公式サイト: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
**公式サイト: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
**メール:** メーリングリストに登録するために、<a href="mailto:dev-subscribe@hertzbeat.apache.org">dev-subscribe@hertzbeat.apache.org</a>にメールを送ってください。
## 🎡 <font color="green">紹介</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) incubating)はオープンソースリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き。
[Apache HertzBeat](https://github.com/apache/hertzbeat) は AI 駆動の次世代オープンソースリアルタイム観測システムです。メトリクスとログの統一収集、アラートの一元配信、インテリジェント管理分析。エージェント不要、高性能クラスタ、強力なカスタム監視とステータスページ構築機能を提供します。
### 特性
- **モニター・アラーム・通知** を一体化し、Web、データベース、キャッシュ、OS、ミドルウェア、クラウドネイティブ、ネットワークなどをサポート
- 使いやすさを重視し、マウスのクリックだけで完全なWebベースの操作が可能
- `Http、Jmx、Ssh、Snmp、jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8S``Docker`のような新しい監視タイプに素早く対応できることを想像してみてください
- `Prometheus`のエコシステム互換、Prometheusが監視できるものを、全てオンライン設定によって監視することができます
- 高性能で、コレクタークラスタ、マルチアイソレートネットワーク、クラウドエッジをサポート
- 柔軟なアラーム閾値ルールで、メール、discord、slack、telegramなどを通じてタイムリーに通知
- 強力なステータスページを簡単に構築し、リアルタイムのステータスをユーザーと共有
- **収集+分析+アラート+通知**を一つのプラットフォームに統合し、HertzBeat AI 駆動の新しいインタラクションと機能、内蔵 MCP Server 機能を提供
- 統一メトリクスプラットフォーム、エージェントレス、Prometheus互換、アプリケーションサービス、プログラム、データベース、キャッシュ、オペレーティングシステム、ビッグデータ、ミドルウェア、Webサーバー、クラウドネイティブ、ネットワーク、カスタム監視などをサポート
- 統一ログプラットフォーム、OTLP プロトコルを通じて複数のログソースをシームレスに統合してレポート
- 統一アラートプラットフォーム、内部アラートと様々な外部アラートソースを統合接続、統一アラート処理分析、柔軟なリアルタイムと周期的閾値ルール、グループ収束、サイレンス、抑制など
- 統一メッセージ配信、アラートプラットフォームで処理後、`メール` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` などの方法で配信通知
- `Http、Jmx、Ssh、Snmp、Jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8s``Docker`のような新しい監視タイプに素早く対応できることを想像してみてください
- 高性能で、コレクタークラスタの水平拡張、マルチ分離ネットワーク監視、クラウドエッジ協調をサポート
- 強力なステータスページ構築機能を提供し、サービスのリアルタイムステータスをユーザーに簡単に伝達。
> `HertzBeat`の強力なカスタマイズ、多種類のサポート、高性能および容易な拡張性は、ユーザーが迅速に独自の監視システムを構築するのを支援することを目的としています。
> `HertzBeat`の統一プラットフォーム、AI インテリジェンス、強力なカスタマイズ、多種類サポート、高性能、容易な拡張性は、ユーザーが迅速かつ便利に観測要件を実現することを支援することを目的としています。
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 モジュール
## 🥐 モジュール
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
## ⛄ 利用可能
## ⛄ 利用可能
> `mysql`、`Java仮想マシン`、`k8s` などのすべての監視コレクション タイプを `YML` 監視テンプレートとして定義し、ユーザーがそれらをインポートして対応するタイプの監視をサポートできるようにしています。
> `mysql`、`Java仮想マシン`、`k8s` などのすべての監視コレクション タイプを `YML` 監視テンプレートとして定義し、ユーザーがそれらをインポートして対応するタイプの監視をサポートできるようにしています。
> カスタマイズされた一般的なモニターのテンプレートを提供することを歓迎します。
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
@@ -101,7 +93,7 @@
- その他、カスタマイズされたモニターテンプレート。
- 通知利用可能 `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`
## 🐕 クイックスタート
## 🐕 クイックスタート
- HertzBeat をローカルに展開する場合は、以下のデプロイメントドキュメントを参照してください。
@@ -110,7 +102,7 @@
##### 方式1:Docker
1. `docker` で以下の指令を実行します:
1. `docker` で以下の指令を実行します:
```shell
docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat
@@ -163,9 +155,9 @@
##### 方式3:ローカルの実行
1. ローカルの実行には、バックエンドのプロジェクト`manager`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
2. バックエンド:`maven3+`、`java17`と`lombok`の環境必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`manager` を起動します。
3. フロントエンド:`nodejs npm angular-cli`の環境必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `ng serve --open` というコマンドを実行します。
1. ローカルの実行には、バックエンドのプロジェクト`hertzbeat-startup`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
2. バックエンド:`maven3+`、Java 25、`lombok`の環境必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`hertzbeat-startup` を起動します。
3. フロントエンド:`Node.js 22` と `pnpm 10` の環境必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `corepack pnpm@10.9.0 install --frozen-lockfile && corepack pnpm@10.9.0 dev` を実行します。
4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
詳細ステップ [貢献ガイド](CONTRIBUTING.md)
@@ -174,7 +166,7 @@
[Docker-Compose 部署脚本](script/docker-compose)でpostgresql/mysqlデータベース、victoria-metrics、iotdb、またはtdengine時系列データベースとHertzbeat一括デプロイ。
詳細ステップ [通过 Docker-Compose 安装 HertzBeat](script/docker-compose/README.md)
詳細ステップ [通过 Docker-Compose 安装 HertzBeat](script/docker-compose/README.md)
##### 方式5:Kubernetes Helm Charts
@@ -186,8 +178,8 @@ Helm ChartでHertzBeatクラスタコンポーネントをKubernetesクラスタ
## ✨ Contributors
Thanks these wonderful people, welcome to join us:
[貢献ガイド](CONTRIBUTING.md)
Thanks these wonderful people, welcome to join us:
[貢献ガイド](CONTRIBUTING.md)
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
@@ -363,7 +355,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
@@ -387,7 +379,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
@@ -531,6 +523,33 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -538,7 +557,7 @@ Thanks these wonderful people, welcome to join us:
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## 💬 コミュニティ交流
@@ -568,7 +587,7 @@ WeChat公式アカウント : `usthecom`を検索.
HertzBeat is built on so many great open source projects, thanks to them!
- `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...`
- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...`
- `TypeScript Next.js React Tailwind CSS Node.js Npm ECharts G6 MonacoEditor Docusaurus ...`
## Landscape
+176
View File
@@ -0,0 +1,176 @@
# HertzBeat Community Alpha Preview
This document is for the GitHub community alpha preview branch. It is not a
stable Apache HertzBeat release checklist. Use it to try the new entity-centered
observability and Vite React frontend, report issues, and help harden the project
toward a beta.
## What Is In Scope
- Entity-centered observability read models for metrics, logs, traces, topology,
alerts, owners, runbooks, and handoff context.
- Vite React `web-app` workflows for the operational shell, entity management,
topology, instrumentation, and the three-signal query workspace.
- OTLP trace/log/metric query paths backed by the existing warehouse and
Greptime integrations.
- Local demo topology relation seeding for the small Checkout API -> Payment API
-> Orders DB example when the `local` Spring profile is active.
## Local Source Quickstart
1. Start the backend from `hertzbeat-startup` with Java 25.
Keep the Arrow JVM open option when running locally:
```shell
--add-opens=java.base/java.nio=ALL-UNNAMED
```
The local backend listens on `http://127.0.0.1:1157` by default.
2. Start the Vite React frontend:
```shell
cd web-app
corepack pnpm@10.9.0 install --frozen-lockfile
corepack pnpm@10.9.0 dev
```
The preview frontend listens on `http://127.0.0.1:4200`.
3. Open `http://127.0.0.1:4200` and sign in with the local development account
configured for your environment.
## Alpha Validation Checklist
Before opening an alpha issue or pull request, run the smallest check that
matches the area you changed.
Backend entity, topology, observability, and Greptime query changes:
```shell
./mvnw -pl hertzbeat-manager,hertzbeat-observability,hertzbeat-warehouse \
-Dtest=EntityDetailObservabilityReadModelServiceTest,EntityTopologyQueryServiceTest,EntityWorkspaceAccessServiceTest,EntityWorkspaceQueryServiceTest,TraceCallTopologyQueryServiceTest,LocalTopologyDemoRelationSeederTest,LogQueryControllerTest,EntityObservabilityGatewayImplTest,EntityTraceQueryServiceImplTest,GreptimeTraceQueryRepositoryTest,GreptimeDbDataStorageTest \
test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false
```
Startup source package proof:
```shell
./mvnw -pl hertzbeat-startup -am -DskipTests package
```
Topology, entity, observability, shell, session, and shared UI changes:
```shell
cd web-app
corepack pnpm@10.9.0 test -- \
src/features/entity src/features/topology src/features/instrumentation \
src/features/explore
```
Complete frontend release gate:
```shell
cd web-app
corepack pnpm@10.9.0 verify
```
Visible changes also require a production build, a real backend, and a Browser
check of the affected workflow. Do not use mocked topology or signal data as
release evidence.
## Three-Signal SigNoz-Alignment Alpha Cutoff
HertzBeat 2.0 treats SigNoz as a product-shape reference for the alpha
observability workspace, not as a full parity claim. The alpha cutoff is the
operator workflow where metrics, logs, and traces can stay attached to the same
HertzBeat entity context, be saved as reusable views, be composed into
dashboards, and drill from service or operation metrics into related traces and
logs.
This alpha scope includes:
- OTLP metrics, logs, and traces carrying HertzBeat entity context through
`entityId`, `entityType`, `entityName`, service, namespace, environment,
collector, template, and source query parameters.
- Saved query views, dashboard panel drafts, dashboard variables, and persisted
dashboards for metrics, logs, and traces.
- A service overview dashboard with RED-style metrics, Apdex, database calls,
external calls, key operations, logs, log errors, traces, trace errors,
exceptions, exception messages, and firing alerts.
- An operation drilldown dashboard from `operationName` context, with metrics
filtered by `operation`, logs filtered by `http.route`, traces filtered by
`operationName`, and exceptions grouped by `exception.type`.
- Runtime dashboard evidence flows from metric/log/trace points into related
signal handoffs, evidence panel drafts, and breakout panel drafts.
- Stable entity binding from OpenTelemetry resource identity such as
`service.name`, host, and Kubernetes pod attributes.
This alpha scope does not claim full SigNoz parity. In particular, it does not
claim public dashboard sharing, Terraform-managed dashboards, a prebuilt
dashboard template marketplace, a ClickHouse SQL dashboard builder, a log
pipeline builder, cost-meter dashboards, full flamegraph parity, or automatic
APM RED metric derivation from traces beyond the explicit telemetry/query data
seeded or stored in the current HertzBeat runtime.
Runtime signal dimensions such as `trace_id`, `span.name`, `http.route`,
`operation`, `operationName`, `exception.type`, and `exception.message` are
filter, group-by, handoff, and drilldown dimensions. They must not be promoted
into long-lived `ObserveEntity` identities unless a later design explicitly
adds an endpoint or operation entity model.
The focused live proof for this cutoff is:
```shell
TRACE_ID=6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b bash script/dev/run-three-signal-live-proof.sh
```
That proof starts a non-persistent H2 backend, seeds OTLP metrics/logs/traces,
verifies entity binding and signal query breakouts, starts `web-app`, and checks
that the active React routes are served. Product-level browser acceptance remains
a separate real-backend validation step.
## Local Scale Proof Data
Large topology scale proof data is not seeded by default. The local demo seeder
only creates the small demo relation repair during normal `local` profile
startup.
To explicitly seed the local mixed scale proof entity catalog, start the backend
with:
```shell
--hertzbeat.topology.local-scale-proof-seed=true
```
This is intended for local performance investigation only. Do not treat it as
required alpha setup.
## Known Alpha Limitations
- The Vite React frontend is still an alpha preview. Parity claims require
route, action, API read/write, refresh, context-handoff, and Browser evidence.
- Three-signal work is scoped to the alpha cutoff above. Do not describe it as
full SigNoz parity unless a later release adds and proves the omitted product
capabilities.
- Topology large-graph behavior is optimized for inspection with render
windows, table drilldown, and optional browser smoke. Continue filing cases
where real data feels confusing or slow.
- Runtime verification may depend on local H2 or Greptime data shape. Greptime
scale proof fixtures are local-only and should not be committed as generated
proof artifacts.
- Source package rebuilds are expected to pass for the alpha candidate. If a
local module or dependency warning blocks packaging, include the focused
Maven command and output with the issue report.
## Reporting Issues
When filing alpha feedback, include:
- The route or API endpoint.
- The storage mode, for example H2-only or Greptime-backed traces.
- The browser viewport and whether the in-app refresh was used.
- Expected versus actual node, edge, table, or handoff behavior.
- Any focused test, Maven, Vitest, ESLint, or browser-smoke command that
reproduces the issue.
@@ -0,0 +1,340 @@
# HertzBeat Hybrid Collector 完整交付计划
> 状态:执行基线
> 最近更新:2026-07-15
> 实施分支:`codex/hybrid-collector-phase0`
> 实施工作树:`/Users/zhaoqingran/IdeaProjects/hertzbeat-hybrid-collector`
## 1. 计划用途
本文件是 Hybrid Collector 后续实现、评审和验收的唯一范围基线。后续会话不得依赖聊天记忆决定范围:
1. 每次只推进一个里程碑;
2. 开始前先读取本文件和 `progress.md`
3. 新增或删除能力必须先修改本文件并说明原因;
4. 验证失败时停在当前里程碑修复,不带失败进入下一项;
5. 所有里程碑和最终验收全部通过后,才能声明 Hybrid Collector 完成。
本计划已经吸收 2026-07-10 Hybrid Collector 架构评审的结论和架构红线,不重新发明另一套采集器。
## 2. 最终产品定义
Hybrid Collector 必须同时具备两类能力:
- **主动采集**:采集主机指标、Prometheus 指标、批准的本地日志,并通过随包提供的官方自动埋点采集 Java 应用链路和 JVM 指标;
- **接收转发**:通过标准 OTLP/HTTP 和 OTLP/gRPC 接收应用或其他 OTel SDK/Agent 的 Metrics、Logs、Traces,经过保护和补充后直接发送 HertzBeat Server。
对用户保持以下产品边界:
- 一个安装包;
- 一个启动入口和系统服务;
- 一个 Collector ID
- Java Collector 是唯一注册、配置和生命周期主控;
- Go OTel Runtime 不作为第二个 Collector 注册;
- Go Runtime 直接向 HertzBeat Server 发送遥测,Java 不代理数据;
- 不要求用户另装第三方 OpenTelemetry Collector
- 默认配置可工作,高级能力通过受控语义配置开启,不允许远程下发任意 YAML。
## 3. 完成后的固定组件清单
使用 OCB 构建裁剪后的官方组件集合。除下表外,不增加组件。
### 3.1 Receiver
| 组件 | 最终用途 | 状态 |
| --- | --- | --- |
| `otlpreceiver` | OTLP HTTP/gRPC 接收 Metrics、Logs、Traces | 已实现 |
| `hostmetricsreceiver` | 主动采集 CPU、内存、磁盘、文件系统、网络、负载、进程指标 | 已实现 |
| `prometheusreceiver` | 主动抓取受管 Prometheus/OpenMetrics 端点 | 已实现 |
| `filelogreceiver` | 主动采集本地策略批准的日志文件并处理轮转 | 已实现 |
### 3.2 Processor
| 组件 | 最终用途 | 状态 |
| --- | --- | --- |
| `memorylimiterprocessor` | 有界内存和快速拒绝 | 已实现 |
| `batchprocessor` | 三信号有界批处理 | 已实现 |
| `resourceprocessor` | 写入 HertzBeat Collector/环境等固定资源属性 | 已实现 |
| `resourcedetectionprocessor` | 检测 host、OS、process、container 和获准的云环境资源 | 已实现 |
| `attributesprocessor` | 删除禁止属性并补充固定低基数属性 | 已实现 |
| `filterprocessor` | 使用产品预置规则过滤健康探测等明确噪声;不开放任意远程 OTTL | 已实现 |
### 3.3 Exporter、Extension 和 Provider
| 组件 | 最终用途 | 状态 |
| --- | --- | --- |
| `otlphttpexporter` | 直接发送 HertzBeat OTLP HTTP 接口 | 已实现 |
| `filestorage` | 日志 offset 和三信号持久发送队列 | 已实现 |
| `healthcheckextension` | Java Supervisor 的本地健康判断 | 已实现 |
| `bearertokenauthextension` | 非 loopback OTLP Gateway 模式的入站认证 | 已实现 |
| `envprovider` / `fileprovider` / `yamlprovider` | 读取 Java 生成配置和环境 secret | 已实现 |
### 3.4 自动埋点
自动埋点不是 Collector Contrib Receiver,但属于完整交付范围:
| 能力 | 实现方式 | 状态 |
| --- | --- | --- |
| Java 应用 Trace | 随包提供官方 OpenTelemetry Java Agent | 待实现 |
| JVM Runtime Metrics | 官方 Java Agent runtime instrumentation | 待实现 |
| 日志 Trace/Span 关联 | Java Agent 注入日志上下文,日志由 File Log 或 OTLP Logs 进入 | 待实现 |
| 其他语言 | 提供标准本地 OTLP 地址、Header 和 Resource 配置,不捆绑其他语言 Agent | 待实现文档和 smoke |
不得通过伪造 Span 或把普通日志推断成 Trace 来满足“三信号”。链路必须来自真实应用执行或标准 OTLP 上游。
## 4. 明确不在本次交付范围
以下能力不进入本计划,也不能在实现过程中顺手加入:
- 完整 Contrib Distribution
- 任意 Raw OpenTelemetry YAML
- 自研 OTLP Receiver、Exporter、队列、存储或日志 tail
- Java 中转遥测数据;
- JNI、JNA、cgo 或 fork OpenTelemetry Collector Core
- 自动扫描并注入任意本机进程;
- 捆绑 Python、Node.js、.NET 和 Go 自动埋点发行物;
- 边缘 Tail Sampling、Span Metrics 和 Service Graph;这些需要全局 Trace 视角,属于 HertzBeat Server
- Kubernetes 全家桶、Docker Stats、云厂商、数据库和中间件 Contrib Receiver;现有 HertzBeat Java Agentless 能力继续负责这些目标,独立需求另行评审;
- OpAMPManager 到 Java Collector 的现有控制通道继续作为唯一控制面;
- 自定义 Go control extension、热重载和 Runtime 在线升级;
- pprof、zPages 等默认生产诊断端口;
- 远程下发任意绝对日志路径、明文 Token 或证书私钥。
## 5. 固定里程碑
### M1:服务端 Desired Config 和安全切换
状态:**已完成**`353aeadcd`
交付内容:
- Server 持久化版本化 `ManagedOtelRuntimeConfig`
- 复用现有 Collector 心跳响应下发,不增加新协议和消息类型;
- Collector 只接受严格递增 revision
- Java 渲染与随包 Runtime 版本匹配的 YAML
- 新配置先生成 candidate、实际 Runtime validate,再原子激活;
- validate 失败不停止当前 Runtime
- readiness 失败只回滚一次 last-known-good
- 配置存储或解析失败不影响普通 Collector 心跳和 Agentless 任务;
- 管理 API 支持查询和更新语义配置;
- H2、MySQL、PostgreSQL 迁移齐全。
完成门禁:
- Manager 配置服务、API、心跳新旧版本兼容测试;
- Collector 下发解析、revision、candidate/LKG 和进程切换测试;
- Java 25 Checkstyle、Spring Boot 4 AOT
- `git diff --check`
### M2:资源识别、归属和固定数据治理
状态:**已完成**`652f2a836`
交付内容:
- 加入 `resourcedetectionprocessor``attributesprocessor``filterprocessor`
- 检测 host、OS、process、container;云环境 detector 仅在显式启用时运行;
- 统一三信号上的 Collector ID、runtime version、service/environment 属性;
- 明确用户属性、检测属性和 HertzBeat 固定属性的覆盖顺序;
- 过滤规则使用产品枚举,不接受任意 OTTL;
- 删除认证 Header、Token、cookie 等禁止属性;
- 高基数属性不自动提升为资源维度;
- 三信号保持相同的资源归属语义。
完成门禁:
- 三信号资源属性一致性测试;
- 属性覆盖、脱敏、过滤和错误配置测试;
- OCB validate 和真实 Runtime export smoke
- 资源识别关闭时无额外云 metadata 请求。
### M3:主动指标与日志采集闭环
状态:**已完成**`f27b2f184`
交付内容:
- Host Metrics 支持采集间隔和 scraper 白名单;
- Prometheus 支持最多 32 个受管端点、HTTPS、抓取间隔、超时和静态 Header secret 引用;
- File Log 只使用本机管理员定义的 path profile
- realpath、符号链接逃逸、deny path、glob 数、文件数、单行大小和读取速率均有上限;
- 日志默认 `start_at: end`,轮转和 copytruncate 行为明确;
- 文件 offset 和 exporter queue 共用 owner-only storage
- Manager 展示 source 的 desired、active、rejected 状态及最后错误;
- 删除或修改 source 后不会留下失效 pipeline。
完成门禁:
- Linux/macOS/Windows 路径策略单元测试;
- Prometheus 超时、TLS、错误响应和高基数样本测试;
- File Log 轮转、截断、重启 offset、拒绝敏感路径测试;
- Host Metrics、Prometheus、File Log 同时运行的真实 Runtime 测试。
### M4:安全 OTLP 接收和 Gateway 模式
状态:**已完成**`323f34b83`,许可证制品修复 `4eab2fa3e`
交付内容:
- Agent 模式默认只监听 `127.0.0.1:4317/4318`
- HTTP/gRPC 三信号、Protobuf/JSON 协议覆盖;
- 单请求大小、并发、队列和超时有界;
- Gateway 模式必须显式开启,非 loopback 监听必须配置 TLS
- Gateway 模式加入官方 Bearer Token Auth,可选 mTLS
- 认证 secret 只通过环境或本地 secret 文件传递;
- 认证失败、非法 payload、超限分别返回明确协议错误;
- 入站流量与主动采集共享总资源预算,不能互相饿死。
完成门禁:
- OTLP HTTP JSON/Protobuf 和 gRPC 三信号真实客户端测试;
- 无鉴权、错误 Token、过期/轮换 Token、TLS 和 mTLS 测试;
- 4 MiB 边界、并发拒绝和停止接收时的优雅排空测试;
- 默认配置确认没有公网监听。
### M5:随包 Java 自动埋点
状态:**进行中**
交付内容:
- 固定并审核官方 OpenTelemetry Java Agent 版本;
- Agent 作为独立可选制品放入安装包,不进入 Native Java Collector 可达闭包;
- 提供生成器输出 `-javaagent`、OTLP endpoint、protocol、resource attributes 和日志关联参数;
- Agent 默认发送到本机 Hybrid Collector,不直接绕过 Collector
- 支持用户显式启用/禁用 instrumentation
- 不自动修改用户启动脚本,不 attach 任意运行中 JVM
- 提供 Spring Boot、普通 JAR 和容器三种可复制启动方式;
- File Log 与 Agent 注入的 trace/span 字段能够关联;
- Agent 缺失、版本不兼容或端口不可用时给出明确诊断。
完成门禁:
- 一个真实 Java 示例应用产生 HTTP、数据库、异常和异步 Span;
- 同一应用产生 JVM 指标、关联日志和 Trace;
- HertzBeat Server 可查询三信号并按 traceId 跳转;
- Agent 关闭时应用行为不受 Collector 影响;
- Agent LICENSE、NOTICE、SBOM 和漏洞扫描通过。
### M6:运行状态、内部遥测和故障诊断
状态:**待实现**
交付内容:
- Java 心跳报告 desired revision、active revision、进程状态、PID、重启次数和最后错误;
- Go Runtime internal telemetry 直接上报 HertzBeat Server
- 展示 accepted、refused、sent、failed、queue size/capacity 和 file consumer 状态;
- 区分配置错误、端口冲突、后端不可用、认证失败、队列满、进程崩溃;
- 后端不可用只触发 exporter retry/queue,不重启 Runtime
- Java Agentless 采集在 Go Runtime 降级或失败时继续工作;
- 日志中不打印 Token、Authorization、完整用户日志正文或证书内容。
完成门禁:
- 每类故障的状态收敛测试;
- 心跳负载和 Server 查询不会携带遥测正文;
- Runtime crash/restart circuit 和人工恢复测试;
- 缺数据、不可用和真实零值能够区分。
### M7:容量、背压和长期故障恢复
状态:**待实现**
交付内容:
- 三信号共享内存上限和独立 pipeline 可观测性;
- exporter 磁盘队列固定容量、owner-only 权限和明确满盘行为;
- HertzBeat/Greptime 暂停后不丢已确认进入持久队列的数据;
- Runtime 被强杀、Java 被重启、机器重启后均能恢复队列和日志 offset;
- 无无限重试线程、无限队列或后台泄漏;
- 停止压力后自动恢复,不要求重启 Greptime 或 Collector
- 记录真实 CPU、RSS、磁盘、吞吐和延迟,不伪造容量结论。
完成门禁:
- Metrics、Logs、Traces 独立和混合负载;
- 后端断开、恢复、慢响应、429、503 和连接重置;
- 队列满、磁盘满和损坏存储;
- 24 小时 soak
- 停压恢复与无残留长任务证明。
### M8Java Native、多平台发行和真实端到端验收
状态:**待最终复验;基础打包已实现**
交付内容:
- Java 25 + Spring Boot 4 Collector
- macOS/Linux arm64/amd64、Windows amd64 Native Java 制品;
- 对应平台的 no-CGO Go Runtime
- Java Agent 作为可选独立文件随包发布;
- 单服务启动、停止、信号转发、子进程清理和崩溃恢复;
- Docker Native 镜像非 root 运行且无 JVM
- 组件 inventory、LICENSE、NOTICE、SBOM、漏洞报告、校验和与可复现构建;
- 真实 HertzBeat Server + GreptimeDB 完成用户闭环。
最终浏览器/API 闭环:
1. 注册一个 Collector
2. 配置主机指标、Prometheus 和日志 profile
3. 生成 Java Agent 启动参数并启动真实应用;
4. 查询主动采集指标和日志;
5. 查询应用 Trace、Span 和事件;
6. 日志按 traceId 跳转 Trace
7. Trace 查看同时间窗口日志和指标;
8. 断开 HertzBeat/Greptime 后恢复,积压自动发送;
9. 强杀 Go RuntimeJava Agentless 不停且 Go 自动恢复;
10. 重启整个 Collectoroffset、队列、配置 revision 和身份保持正确。
最终发布门禁:
- 全部聚焦单测、集成测试和真实 Runtime 测试;
- Java 25 Checkstyle、Spring Boot 4 AOT、Native Image
- 五个平台布局与启动合同;
- OCB inventory、license、SBOM、`govulncheck`、校验和和可复现构建;
- HertzBeat/Greptime 三信号真实写入和查询;
- 24 小时 soak 和故障恢复;
- `git diff --check`、无硬编码 CJK、无本地产物进入提交;
- 人工代码评审确认没有重复抽象、自研替代官方组件或未使用代码。
## 6. 实施顺序和停止规则
固定顺序为 `M1 → M2 → M3 → M4 → M5 → M6 → M7 → M8`,不得跳过失败门禁并继续。
每个里程碑执行循环:
1. 写最小失败合同;
2. 做满足合同的最小实现;
3. 运行聚焦验证;
4. 修复全部失败;
5. 运行该里程碑的真实 Runtime 或端到端证明;
6. 更新本文件状态和 `progress.md`
7. 只提交本里程碑相关源码、测试和文档;
8. 进入下一里程碑。
只有以下情况可以暂停并询问维护者:
- 需要改变本文件的固定交付范围;
- 需要新增官方组件或自研组件;
- 出现许可证、安全、跨平台或后端契约阻塞;
- 需要外部 CI、签名或发布权限;
- 真实数据证明原验收指标不可实现,需要维护者决定取舍。
## 7. 完成定义
以下条件必须同时成立:
- 主动采集主机指标、Prometheus 指标和批准的文件日志;
- 随包 Java Agent 从真实应用采集 Trace、JVM 指标并关联日志;
- 安全接收并转发外部 OTLP 三信号;
- 三信号拥有一致资源身份和 HertzBeat Collector 归属;
- Server Desired Config 能安全下发、验证、切换和回滚;
- 后端中断、进程崩溃和机器重启后能够恢复;
- Java Agentless 采集不依赖 Go Runtime 健康;
- JVM 和 Native Collector 均可运行,发布包覆盖目标平台;
- 真实 HertzBeat + Greptime 完成三信号查询与跨信号跳转;
- 所有 M1 到 M8 门禁通过;
- 代码通过人工可维护性评审,无未使用实现和临时兼容层。
未同时满足上述条件时,不得使用“Hybrid Collector 已完成”或“可正常替代 OTel Collector”作为结论。
+46
View File
@@ -0,0 +1,46 @@
# Application Instrumentation API
HertzBeat exposes one public application-instrumentation route family. The
paths are unversioned; internal implementation package names are not alternate
API surfaces. Every response uses the ordinary HertzBeat `Message<T>` envelope.
## Public endpoints
| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/instrumentation/catalog` | Discover source groups, sources, and selectable recipes |
| `GET` | `/api/instrumentation/intake-profiles` | Discover non-secret OTLP intake destinations and transport requirements |
| `POST` | `/api/instrumentation/render` | Render structured onboarding blocks for a recipe and intake profile |
| `POST` | `/api/instrumentation/detect` | Detect scoped Metrics, Logs, and Traces reception |
There are no public path-version aliases. Clients must not prepend a version
segment or construct paths from implementation class or package names.
## Authorization
Catalog and intake-profile discovery are available to `admin`, `user`, and
`guest` roles. Rendering and detection require `admin` or `user`. Shipped
Sureness configurations enumerate the four paths explicitly so authorization
does not imply a second route family.
## Wire compatibility
The existing `schemaVersion` field remains part of the request and response
payloads. It is a payload compatibility guard, not a URL version. This contract
does not add, remove, or rename any wire field or enum value.
- Catalog data contains `schemaVersion`, `groups`, `sources`, and `recipes`.
- Intake-profile discovery returns explicit non-secret destinations,
transports, authentication requirements, and availability state.
- Render requests select a source or recipe, environment, platform, intake
profile, and service identity. Responses contain structured blocks and
declared secret placeholders; no token value is returned.
- Detection requests carry the same selection and identity scope plus
`startedAt`. Responses distinguish `waiting`, `received`, `unsupported`,
`unavailable`, and `error`, and return bounded polling and typed query-jump
context.
Clients should consume server-provided catalog choices and intake profiles
rather than inventing endpoints, package coordinates, health states, or signal
support. Unknown or unavailable data must remain distinguishable from healthy
or zero-valued data.
+49
View File
@@ -0,0 +1,49 @@
# Observability Query Context v1
This document freezes the optional query context shared by the Metrics, Logs, and Traces operator
APIs. It is separate from the application instrumentation catalog, render, and detection contract;
that contract remains unchanged.
## Context hierarchy
Queries narrow data in this order:
`Collector -> service.name -> service.namespace -> deployment.environment.name -> instance -> endpoint`
Each supplied dimension is an exact additional constraint. A row, series, log record, or span that
does not carry a supplied dimension does not match. An implementation must return an honest empty
result instead of falling back to the broader service context.
## Frozen fields
| Query field | Meaning | Metrics | Logs | Traces |
| --- | --- | --- | --- | --- |
| `instance` | OTel Resource `service.instance.id` | data-point label `service_instance_id` | Resource `service.instance.id` | Resource `service.instance.id` |
| `endpoint` | Low-cardinality HTTP route template | data-point label `http_route` | LogRecord attribute `http.route` | Span attribute `http.route` |
`endpoint` is a route template such as `/checkout`. It excludes the HTTP method, query string,
fragment, and concrete URL. Values such as `POST /checkout` and `/checkout?order=42` are invalid.
RPC method, messaging destination, and database operation meanings are not overloaded into this
field. Supporting those protocols requires new versioned fields.
The dedicated fields cannot compete with the same key in a free-form resource, attribute, or metric
filter. Such a request is rejected instead of silently applying ambiguous constraints.
## API coverage
- Metrics console request and returned context.
- Historical log list, surrounding context, overview, trace coverage, trend, and group-by queries.
- Live log SSE filtering.
- Trace list, overview, group-by, detail, and span queries. Detail and span requests retain the
exact trace identifier while also applying the supplied discovery context; an optional `spanId`
further narrows the row set.
Storage access remains behind the existing query services and storage adapters. Controllers only
bind the HTTP parameters and construct the storage-neutral context; they contain no database query
language or storage-specific SQL.
## Compatibility
These fields are optional additions to the existing query APIs. Omitting both retains the prior
behavior. Changing either field's semantic mapping, accepting an HTTP method in `endpoint`, or
reusing `endpoint` for a non-HTTP protocol requires a new contract version.
+215
View File
@@ -0,0 +1,215 @@
# Product Design User Validation Loop
This runbook makes Product Design part of the HertzBeat 2.0 user-layer
validation loop. It is not a screenshot-only audit pass and it is not a visual
polish layer. Use it to close product experience loops around real operator
workflows, honest state feedback, data lifecycle clarity, and repeatable
actions.
## Product Context
Start every Product Design workflow from saved context:
- Product Design state: `$CODEX_HOME/state/plugins/product-design/user-context.md`
- Local screenshot/reference assets:
`$CODEX_HOME/state/plugins/product-design/assets/`
- Repo guidance: `AGENTS.md` and the latest active slice in `progress.md`
- Frontend implementation: `web-app/src/app`, `web-app/src/layout`,
`web-app/src/features`, and `web-app/src/shared`
- Theme and UI constraints: `web-app/src/app/theme`, feature CSS modules,
shared Ant Design primitives, source-backed icon packages, and runtime i18n
catalogs
Saved context should include the HertzBeat product position, operator audience,
competitive references, design preferences, code paths, screenshots, design
system constraints, and mainstream observability console expectations. Future
audits, ideation, prototypes, and implementation work must prefer that context
unless the user explicitly asks for a different direction.
## Required Workflow Selection
Use the smallest workflow that matches the problem.
Small issue:
```text
audit -> small fix -> focused test -> Browser verification -> progress.md
```
Use this for a concrete user-visible problem in one flow, such as confusing
error copy, an unclear button, an empty state that hides the data lifecycle, a
broken cancel path, or a refresh/status mismatch.
Systemic issue:
```text
user-context/get-context -> research -> audit -> ideate -> prototype -> design-qa -> small implementation slice -> progress.md
```
Use this when the issue exposes a broader product pattern, such as monitor
authoring confusion, entity lifecycle ambiguity, alert rule authoring friction,
or settings forms that repeatedly fail to explain required state and side
effects.
Do not skip `audit` or `research` before broad UI changes. Do not use Product
Design to decorate an existing screen while leaving the real workflow,
state feedback, error explanation, or data lifecycle unresolved.
## Design Brief Gate
Before any design judgment, experience redesign, prototype exploration, or UI
implementation, confirm a brief through Product Design `get-context`.
The brief must identify:
- Target user and operating context.
- Workflow problem being solved.
- Existing route, screenshot, design system, or component reference.
- Required interaction fidelity.
- Backend/data lifecycle states that must stay honest.
- Acceptance evidence expected before implementation is considered done.
If the brief is missing or under-specified, stop and ask for only the missing
context. Do not edit UI from an unconfirmed brief.
## Research Gate
For systemic problems, perform product and user research before designing a
direction. Cover:
- Operator mental model and repeated-use pressure.
- Comparable observability platform patterns, especially Grafana, Datadog,
SigNoz, and OpenTelemetry ecosystem consoles where relevant.
- State, error, loading, empty, and recovery language.
- Help affordance density and progressive disclosure.
- Safe authoring, save/cancel, detect/test, refresh, and status convergence.
Prioritize monitor, entity, alert, and settings workflows because these are
high-frequency operator surfaces.
## Real Flow Audit
Audits must use current product evidence:
- Production frontend build.
- Real HertzBeat backend, using H2/Greptime/warehouse data honestly.
- In-app Browser screenshots for the current route state.
- Step notes tied to every screenshot and finding.
- Theme preflight before accepting screenshots: record `html[data-theme]`,
`body[data-theme]`, `--background`, `--card`, and `--ops-background`.
HertzBeat's default audit baseline is `dark-ops` (`--background: 220 11%
5%`, `--ops-background: #0b0c0e`). If an existing Browser origin has
persisted `light-ops`, do not use those screenshots as dark-baseline
evidence; use an isolated localhost port/origin or explicitly document that
the slice is auditing light theme.
Audit at least the relevant parts of:
- Login and guarded deep-link return.
- Navigation and retained query/context parameters.
- Create, edit, save, cancel, detect/test, refresh, and route revisit.
- Status convergence without browser reload when the UI promises refresh.
- Empty, loading, unavailable, failed, and recovered states.
- Help question marks, button labels, destructive confirmations, and action
meaning.
- Card hierarchy, table density, form grouping, text overflow, responsive
behavior, keyboard/focus semantics, and visible accessibility risks.
Findings must cite screenshot names and step numbers. Do not evaluate from
memory.
## Ideation Gate
When audit and research show a systemic UX problem, do not immediately rewrite
product code. Generate two or three different directions first. Useful default
directions for HertzBeat include:
- Dense operations console: fastest scanning and repeat actions.
- Diagnosis-first evidence chain: status, raw backend reason, metric evidence,
history availability, and recovery steps.
- Guided authoring: lower misoperation risk for complex create/edit/test flows.
Choose a direction before prototyping or implementing.
## Prototype Gate
For high-impact flows, build a local interactive prototype before production
implementation. Prototype only the smallest slice needed to validate interaction
and information architecture. Good candidates include:
- Monitor create, detect, detail refresh, and history chart availability.
- Alert rule authoring, preview/test, save/cancel, and template selection.
- Entity create/edit/detail and definition import/edit handoff.
- Settings forms with required fields, disabled save, cancel, and no-write
guarantees.
The prototype is evidence for product decisions. It does not replace
production code, tests, or Browser verification.
## Implementation Gate
Use Product Design `image-to-code` or production UI implementation only after
there is a confirmed brief plus a concrete visual or interaction target:
screenshot, selected visual direction, prototype, Figma frame, or mockup.
Implementation must preserve:
- Existing HertzBeat route/controller/view-model patterns.
- Shared `@hertzbeat/ui` primitives and source-backed icons.
- Dense operational layouts and stable dimensions.
- Clear data lifecycle and backend-state honesty.
- Runtime i18n rules and no hardcoded localized UI copy outside approved
locale resources.
Do not introduce marketing-page composition, decorative chrome, fake zero
states, fake healthy states, broad refactors, or untested UI rewrites.
## Design QA Gate
After implementation, run design QA against the target evidence:
- Original audit screenshot(s).
- Selected direction or prototype.
- Actual production page rendered through the in-app Browser.
- Focused automated tests and the smallest relevant Browser proof.
Check layout, hierarchy, density, text overflow, responsive behavior, control
states, focus/keyboard semantics, empty/error/loading states, and visible
accessibility risks. P0, P1, and P2 design issues must be fixed before the
slice is treated as passing.
## Artifact And Progress Policy
Persist Product Design evidence locally. Use clear names that can be understood
without opening the file.
Recommended local artifact layout:
```text
.tmp/product-design/
<slice-id>/
screenshots/
audit.md
research.md
directions.md
prototype-notes.md
design-qa.md
```
Do not commit `.tmp/`, screenshots, reports, runtime logs, local proof data, or
generated artifacts unless maintainers explicitly ask. The active slice in
`progress.md` must record:
- Saved Product Design context used.
- Research sources or research scope.
- Audit route, backend, screenshots, and steps.
- Theme preflight evidence and whether the screenshot baseline is `dark-ops`
or an intentional alternate theme.
- Findings by severity and user impact.
- Selected direction and prototype target, when applicable.
- Implementation files changed.
- Focused tests and Browser verification.
- Remaining risks and next slice.
The release gate is not complete until Product Design evidence supports the
actual operator workflow, not just a single screenshot or isolated component.
+184
View File
@@ -0,0 +1,184 @@
<!--
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.
-->
# Lightweight React Frontend Architecture
## Status And Reference Baselines
This document is the implementation contract for the maintainable React
single-page application that replaces the deprecated Next.js frontend.
- Implementation baseline: `2.0.0` at `2de48d2caa43fa58fd93e71b0e22e7d4f431c9d5`.
- Angular behavior and release reference: `apache/master` at
`9bd2796119b558cff2834dd35e3d21f2eb4a53a4`.
- Transitional signal reference: `feature/angular-three-signals` at
`5d523ddce322ad82ab1bcb65b3dbe516b0a1b97d`.
The branches have materially diverged. `apache/master` is a behavior and
packaging reference, not a merge source. Relevant fixes are selected and
reimplemented; the branch is never merged wholesale into `2.0.0`.
## Sources Of Truth
Use the following precedence for implementation decisions:
1. `apache/master` defines established operator workflows, stable URLs,
monitor hierarchy behavior, translations, and the external `web-app/dist`
release layout.
2. `2.0.0` defines entity, topology, OTLP signals, workspace and token scope,
GreptimeDB integration, and the current HttpOnly UI session boundary.
3. The Angular three-signal transition defines the simple onboarding and
query workflow that does not require a user to understand storage internals.
4. The removed Next.js implementation supplied only proven DTOs and pure
algorithms. Its page structure, private UI framework, BFF, compatibility
scaffolding, and implementation-detail tests were not migration sources.
## Scope Allowlist
### Established Workflows
- Session, lock, logout, permissions, locale, theme, shell notifications, and
account actions.
- Dashboard and Overview as aliases for one implementation, Bulletin, and the
public status page.
- Monitor list, create, edit, copy, detect, bulk operations, detail, realtime,
history, and favorite metrics.
- Alert center, thresholds, groups, inhibition, silence, notice, and integration.
- Collector, monitor definitions, labels, plugins, status, system configuration,
and message server settings.
- Dynamic monitor categories loaded from `/apps/hierarchy`.
### Approved 2.0 Extensions
- Entity list, create, edit, detail, import, discovery, and definition.
- Topology query, graph inspection, table drilldown, and detail interaction.
- Unified OTLP integration plus Metrics, Logs, and Traces workbenches.
- Object store and token settings required by observability workflows.
- Workspace, token scope, GreptimeDB, performance protection, and cross-signal
navigation context.
### Excluded From The New Frontend
- Actions, Incidents, Explorer, the standalone Events experiment, AI Chat UI,
MCP settings UI, and the signal dashboard draft/composition editor.
- The production UI Lab, parity framework, source-string ownership assertions,
generated demo states, in-memory fallbacks, and permanent compatibility pages.
- Next.js route handlers, the standalone Node server, the dedicated Next image,
and the Next-specific gateway split.
Deleted routes use the common not-found page. Compatibility URLs are explicit
redirects in the route registry and never duplicate a page implementation.
## Target Stack And Production Shape
- React 19 with Vite and React Router declarative routing.
- TanStack Query as the only server-state lifecycle.
- Ant Design React as the primitive component system. HertzBeat only owns
domain composites such as the time toolbar, signal workbench, data state,
attribute table, and topology canvas.
- i18next with locale JSON resources based on the five `apache/master` locales.
- CSS Modules and Ant Design theme tokens; no Tailwind utility layer.
- ECharts, G6, CodeMirror, and the log virtualizer load only in their owning
features.
- Vitest, React Testing Library, MSW, and Playwright for behavior-level tests.
- Node 22 and pnpm 10 for development and CI. Release assemblies run only the
JVM and never ship a Node runtime or a separate frontend service.
Vite emits `web-app/dist`. Release assemblies copy that directory to the
external distribution `dist/`, matching `apache/master`. Spring serves the SPA
and `/api` from one origin. The frontend is not embedded into the startup jar,
and no Maven frontend plugin is introduced.
## Source Ownership
```text
web-app/src/
app/ bootstrap, providers, route registry, startup
core/ API client, session, errors, i18n, permissions
layout/ basic, passport, and blank layouts
features/ domain-owned routes, API, model, hooks, pages, components
shared/ domain-neutral composites, forms, time, and query context
assets/i18n/ locale JSON resources
```
Each feature owns its route, API, types, hooks, pages, components, and tests.
`core` and `shared` cannot depend on a feature. Features cannot import another
feature's internals. Cross-feature behavior is exposed through a small explicit
feature entry.
Pages orchestrate route state and feature hooks. API modules perform requests
and response mapping. Hooks own TanStack Query lifecycles. Models contain domain
types and pure transformations. Components render visible behavior. A
controller or view model is introduced only when it removes real complexity;
parameter forwarding is not an abstraction.
The typed route registry is the only static source for routes, navigation,
breadcrumbs, permissions, canonical URLs, and redirects. Dynamic monitor
categories are appended after startup and are not duplicated in static source.
## Session And Routing Boundaries
The React migration must not copy the Angular local-storage token boundary.
Spring exposes UI session create, read, refresh, and delete endpoints backed by
the existing account service. Access and refresh tokens remain HttpOnly
cookies. A filter before Sureness adapts the UI access cookie to an internal
Bearer header only when the request has no explicit `Authorization` header.
Existing API tokens, OTLP Bearer authentication, workspace claims, and token
scope remain unchanged.
Cookie-authenticated mutations require same-origin and CSRF validation. Query
requests may refresh through one single-flight request and retry once.
Mutations are never replayed automatically. Same-origin SSE uses the same
session filter and reconnects with bounded backoff.
SPA fallback accepts only `GET` or `HEAD` HTML navigation for registered UI
prefixes. API, actuator, Swagger, H2, OTLP, SSE, and missing static resources
must retain their own non-HTML errors.
## Maintainability Gates
- Page orchestration files: at most 300 lines.
- Ordinary components, hooks, API modules, and models: at most 400 lines.
- Every production file: at most 600 lines.
- Feature public entries: at most 100 lines.
- Functions: at most 100 lines and cyclomatic complexity at most 12.
- No page or component calls `fetch` directly.
- No production mock, in-memory fallback, permanent `compat`, `legacy`, or
`deprecated` implementation directory.
- No tests that read production source and assert source strings.
- Knip and an explicit dependency-boundary check run in CI.
- Production frontend target: no more than 80,000 lines; hard limit 100,000.
- The final migration must delete at least 40 percent of the previous
production frontend source and must not retain the deprecated Next.js tree.
- Initial shell JavaScript: at most 450 KiB gzip. ECharts, G6, and CodeMirror
are forbidden from the shell chunk.
The budgets are architecture limits, not encouragement to split cohesive code
into one-line wrappers. Any temporary exception must name an owner, reason, and
expiry milestone and must be removed before cutover.
## Delivery Gates
Implementation proceeds in vertical milestones: foundation; session and
release; established shell and dashboard; monitors, alerts, and settings;
entities and topology; OTLP signals; then cutover and deletion. Each milestone
must pass focused tests, production build, and its real-backend browser flow
before the next milestone starts.
The final release must contain one JVM runtime, external hashed SPA assets,
working deep links, JSON API 404 responses, authenticated SSE, explicit Bearer
precedence, and no Node runtime or Next-specific gateway.
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ftp",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorFTP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "api",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorHTTP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ping",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorPing}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "port",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorPort}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "fullsite",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorSiteMap}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ssl_cert",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorSSL}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "udp_port",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorUDP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "website",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorWebsite}}"
},
"params": [
+1 -1
View File
@@ -4,7 +4,7 @@
"intervals": 10,
"tags": [],
"app": "kubernetes",
"host": "172.29.0.11",
"instance": "172.29.0.11",
"name": "Brave_Stingray_55yR"
},
"collector": "",
+1 -1
View File
@@ -57,7 +57,7 @@ items:
"credential": "hertzbeat"
}
expect:
statusCode: 409
statusCode: 500
- name: monitorList
request:
api: /api/monitors?pageIndex=0&pageSize=8
-85
View File
@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>hertzbeat-ai-agent</artifactId>
<version>${hertzbeat.version}</version>
<properties>
<spring-ai.version>1.0.1</spring-ai.version>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common</artifactId>
</dependency>
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,41 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import java.util.List;
/**
* Interface that provides access to monitor information by retrieving monitor data
* through the underlying monitor service.
*/
public interface MonitorServiceAdapter {
Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels
);
}
@@ -1,100 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
/**
* Implementation of the MonitorServiceAdapter interface that provides access to monitor information
* through reflection by invoking the underlying monitor service implementation.
*/
@Slf4j
@Component
public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
@Override
public Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels) {
try {
// Provide default values for all nullable parameters
if (sort == null || sort.trim().isEmpty()) {
sort = "gmtCreate";
}
if (order == null || order.trim().isEmpty()) {
order = "desc";
}
if (pageIndex == null) {
pageIndex = 0;
}
if (pageSize == null) {
pageSize = 8;
}
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found: {}", monitorService.getClass().getSimpleName());
Method method = monitorService.getClass().getMethod(
"getMonitors",
List.class, String.class, String.class, Byte.class,
String.class, String.class, int.class, int.class, String.class);
@SuppressWarnings("unchecked")
Page<Monitor> result = (Page<Monitor>) method.invoke(
monitorService,
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result.getContent());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMonitors", e);
} catch (Exception e) {
log.debug("Failed to invoke getMonitors via adapter", e);
throw new RuntimeException("Failed to invoke getMonitors via adapter", e);
}
}
}
@@ -1,246 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.config;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.usthe.sureness.mgt.SurenessSecurityManager;
import com.usthe.sureness.subject.SubjectSum;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Custom Server-Sent Events transport provider for Model Context Protocol.
*/
@Slf4j
public class CustomSseServerTransport implements McpServerTransportProvider {
private final ObjectMapper objectMapper;
private final String messageEndpoint;
private final String sseEndpoint;
private final String baseUrl;
@Getter
private final RouterFunction<ServerResponse> routerFunction;
@Setter
private McpServerSession.Factory sessionFactory;
private final Map<String, Object> sessionRequest = new HashMap<>();
private final ConcurrentHashMap<String, McpServerSession> sessions;
private volatile boolean isClosing;
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
this(objectMapper, messageEndpoint, "/sse");
}
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
this(objectMapper, "", messageEndpoint, sseEndpoint);
}
public CustomSseServerTransport(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) {
this.sessions = new ConcurrentHashMap();
this.isClosing = false;
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.notNull(baseUrl, "Message base URL must not be null");
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
this.objectMapper = objectMapper;
this.baseUrl = baseUrl;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.routerFunction = RouterFunctions.route().GET(this.sseEndpoint, this::handleSseConnection).POST(this.messageEndpoint, this::handleMessage).build();
}
public Mono<Void> notifyClients(String method, Object params) {
if (this.sessions.isEmpty()) {
log.debug("No active sessions to broadcast message to");
return Mono.empty();
} else {
log.debug("Attempting to broadcast message to {} active sessions", this.sessions.size());
return Flux.fromIterable(this.sessions.values())
.flatMap((session) -> session.sendNotification(method, params)
.doOnError((e) -> log.error("Failed to send message to session {}: {}", session.getId(), e.getMessage()))
.onErrorComplete())
.then();
}
}
public Mono<Void> closeGracefully() {
return Flux.fromIterable(this.sessions.values()).doFirst(() -> {
this.isClosing = true;
log.debug("Initiating graceful shutdown with {} active sessions", this.sessions.size());
}).flatMap(McpServerSession::closeGracefully).then().doOnSuccess((v) -> log.debug("Graceful shutdown completed"));
}
private ServerResponse handleSseConnection(ServerRequest request) {
log.debug("Handling SSE connection for request: {}", request);
HttpServletRequest servletRequest = request.servletRequest();
try {
log.debug("Processing SSE connection for servlet request: {}", servletRequest);
log.debug("Authorization header: {}", servletRequest.getHeader("Authorization"));
} catch (Exception e) {
log.error("Authentication failed for SSE connection: {}", e.getMessage());
return ServerResponse.status(HttpStatus.UNAUTHORIZED).body("Unauthorized: " + e.getMessage());
}
if (this.isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
} else {
String sessionId = UUID.randomUUID().toString();
log.debug("Generated session ID for SSE connection: {}", sessionId);
log.debug("Creating new SSE connection for session: {}", sessionId);
return ServerResponse.sse((sseBuilder) -> {
sseBuilder.onComplete(() -> {
log.debug("SSE connection completed for session: {}", sessionId);
this.sessions.remove(sessionId);
});
sseBuilder.onTimeout(() -> {
log.debug("SSE connection timed out for session: {}", sessionId);
this.sessions.remove(sessionId);
});
CustomSseServerTransport.WebMvcMcpSessionTransport sessionTransport = new CustomSseServerTransport.WebMvcMcpSessionTransport(sessionId, sseBuilder);
McpServerSession session = this.sessionFactory.create(sessionTransport);
this.sessionRequest.put(sessionId, request.servletRequest());
this.sessions.put(sessionId, session);
try {
sseBuilder.id(sessionId).event("endpoint").data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
} catch (Exception e) {
log.error("Failed to send initial endpoint event: {}", e.getMessage());
sseBuilder.error(e);
}
}, Duration.ZERO);
}
}
private ServerResponse handleMessage(ServerRequest request) {
if (this.isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
} else if (request.param("sessionId").isEmpty()) {
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
} else {
String sessionId = (String) request.param("sessionId").get();
McpServerSession session = (McpServerSession) this.sessions.get(sessionId);
log.debug("Authorization header for message request: {}", request.servletRequest().getHeader("Authorization"));
SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(sessionRequest.get(sessionId));
McpContextHolder.setSubject(subject);
if (session == null) {
return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));
} else {
try {
String body = request.body(String.class);
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, body);
session.handle(message).block();
return ServerResponse.ok().build();
} catch (IOException | IllegalArgumentException e) {
log.error("Failed to deserialize message: {}", ((Exception) e).getMessage());
return ServerResponse.badRequest().body(new McpError("Invalid message format"));
} catch (Exception e) {
log.error("Error handling message: {}", e.getMessage());
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));
}
}
}
}
private class WebMvcMcpSessionTransport implements McpServerTransport {
private final String sessionId;
private final ServerResponse.SseBuilder sseBuilder;
WebMvcMcpSessionTransport(String sessionId, ServerResponse.SseBuilder sseBuilder) {
this.sessionId = sessionId;
this.sseBuilder = sseBuilder;
log.debug("Session transport {} initialized with SSE builder", sessionId);
}
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
try {
String jsonText = CustomSseServerTransport.this.objectMapper.writeValueAsString(message);
this.sseBuilder.id(this.sessionId).event("message").data(jsonText);
log.debug("Message sent to session {}", this.sessionId);
} catch (Exception e) {
log.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
this.sseBuilder.error(e);
}
});
}
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return (T) CustomSseServerTransport.this.objectMapper.convertValue(data, typeRef);
}
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
log.debug("Closing session transport: {}", this.sessionId);
try {
this.sseBuilder.complete();
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
} catch (Exception e) {
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
}
});
}
public void close() {
try {
this.sseBuilder.complete();
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
} catch (Exception e) {
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
}
}
}
}
@@ -1,37 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration class for Large Language Model (LLM) settings.
*/
@Configuration
public class LlmConfig {
@Bean
public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
return ChatClient.create(chatModel);
}
}
@@ -1,53 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.config;
import org.springframework.stereotype.Component;
/**
* Provider for system prompts used in the AI agent
*/
@Component
public class PromptProvider {
/**
* Static version of the HertzBeat monitoring prompt
*/
public static final String HERTZBEAT_MONITORING_PROMPT = """
You are an AI assistant specialized in monitoring infrastructure and applications with HertzBeat.
Your role is to help users manage and analyze their monitoring data using the available tools.
You have access to the following HertzBeat monitoring tools:
- list_monitors: Query monitor information with flexible filtering and pagination
- add_monitor: Add a new monitor to the system
When users ask questions about their monitoring setup or data, identify which tool would be most helpful
and use it to provide relevant information. Always provide clear explanations of the monitoring data and
suggest next steps or insights based on the results.
For monitoring-related queries:
1. If users want to see their monitors, use list_monitors with appropriate filters
2. If users want to add a new monitor, use add_monitor with the necessary details
3. If the monitoring information shows potential issues, highlight them and suggest troubleshooting steps
For parameters that accept specific values:
- Monitor status values: 0 (no monitor), 1 (usable), 2 (disabled), 9 (all)
- Sort fields typically include: name, host, app, gmtCreate
- Sort order should be 'asc' or 'desc'
Keep responses focused on monitoring topics and HertzBeat capabilities.
If you're unsure about specific monitoring details, ask clarifying questions before using the tools.
""";
}
@@ -1,70 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
/**
* Controller class for handling chat-related HTTP requests.
*/
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClientProviderService chatClientProviderService;
@Autowired
public ChatController(@Qualifier("openAiChatClient") ChatClient openAiChatClient,
ChatClientProviderService chatClientProviderService) {
this.chatClientProviderService = chatClientProviderService;
}
/**
* Send a message and get a streaming response
*
* @param context The chat request context containing message and optional
* conversationId
* @return SSE emitter for streaming response
*/
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(@RequestBody ChatRequestContext context) {
SseEmitter emitter = new SseEmitter();
new Thread(() -> {
try {
String aiResponse = chatClientProviderService.streamChat(context);
emitter.send(aiResponse);
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
}
@@ -1,26 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.controller;
/**
* Controller for managing conversations.
*/
public class ConversationController {
}
@@ -1,25 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.dao;
/**
* Data Access Object interface for Conversation entities.
*/
public interface ConversationDao {
}
@@ -1,25 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.dao;
/**
* Data Access Object interface for Message entities.
*/
public interface MessageDao {
}
@@ -1,25 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.dao;
/**
* Data Access Object interface for UserPreference entities.
*/
public interface UserPreferenceDao {
}
@@ -1,26 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.service;
/**
* Service interface for agent operations.
*/
public interface AgentService {
}
@@ -1,31 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
/**
* Service for interacting with LLM providers (like OpenAI, Anthropic, etc.)
*/
public interface ChatClientProviderService {
String complete(String message);
String streamChat(ChatRequestContext context);
}
@@ -1,71 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
import java.util.Map;
/**
* Service for managing chat conversations and interactions with LLM providers.
*/
public interface ConversationService {
/**
* Send a message and receive a streaming response
*
* @param message The user's message
* @param conversationId Optional conversation ID for continuing a chat
* @return SseEmitter for streaming the response
*/
SseEmitter streamChat(String message, String conversationId);
/**
* Send a message and get a complete response
*
* @param message The user's message
* @param conversationId Optional conversation ID for continuing a chat
* @return Response object containing the AI's response and conversation metadata
*/
Map<String, Object> chat(String message, String conversationId);
/**
* Get conversation history for a specific conversation
*
* @param conversationId Conversation ID
* @return Conversation data including messages
*/
Map<String, Object> getConversation(String conversationId);
/**
* Get all conversations for the current user
*
* @return List of conversations
*/
List<Map<String, Object>> getAllConversations();
/**
* Delete a conversation
*
* @param conversationId Conversation ID to delete
*/
void deleteConversation(String conversationId);
}
@@ -1,30 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import org.apache.hertzbeat.ai.agent.service.AgentService;
import org.springframework.stereotype.Service;
/**
* Implementation of the AgentService interface.
* This service provides functionality for handling AI agent operations.
*/
@Service
public class AgentServiceImpl implements AgentService {
}
@@ -1,70 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import org.apache.hertzbeat.ai.agent.config.PromptProvider;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
/**
* Implementation of the {@link ChatClientProviderService}.
* Provides functionality to interact with the ChatClient for handling chat
* messages.
*/
@Service
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
private final ChatClient chatClient;
@Qualifier("hertzbeatTools")
@Autowired
private ToolCallbackProvider toolCallbackProvider;
@Autowired
public ChatClientProviderServiceImpl(@Qualifier("openAiChatClient") ChatClient openAiChatClient) {
this.chatClient = openAiChatClient;
}
@Override
public String complete(String message) {
return this.chatClient.prompt()
.user(message)
.call()
.content();
}
@Override
public String streamChat(ChatRequestContext context) {
try {
return this.chatClient.prompt(PromptProvider.HERTZBEAT_MONITORING_PROMPT)
.user(context.getMessage())
.toolCallbacks(toolCallbackProvider)
.call()
.content();
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
}
@@ -1,29 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import org.springframework.stereotype.Service;
/**
* Implementation of the ConversationService interface for managing chat conversations.
*/
@Service
public class ConversationServiceImpl {
}
@@ -1,82 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.agent.service.McpServerService;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.tools.impl.MonitorToolsImpl;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Autowired;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
/**
* Implementation of the McpServerService interface.
* This service provides functionality for handling MCP server operations.
*/
@Service
@Configuration
public class McpServerServiceImpl implements McpServerService {
@Autowired
private MonitorToolsImpl monitorTools;
@Bean
public ToolCallbackProvider hertzbeatTools() {
return MethodToolCallbackProvider.builder().toolObjects(monitorTools).build();
}
/**
* Provides a custom SSE server transport for the MCP server.
*
* @param objectMapper the ObjectMapper instance for JSON serialization
* @param serverProperties the properties for the MCP server configuration
* @return a CustomSseServerTransport instance configured with the provided properties
*/
@Bean
public CustomSseServerTransport webMvcSseServerTransportProvider(
ObjectMapper objectMapper,
McpServerProperties serverProperties
) {
return new CustomSseServerTransport(
objectMapper,
serverProperties.getBaseUrl(),
serverProperties.getSseMessageEndpoint(),
serverProperties.getSseEndpoint()
);
}
/**
* Provides the MCP server transport bean.
*
* @param transport the custom SSE server transport
* @return the MCP server transport instance
*/
@Primary
@Bean
public RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport) {
return transport.getRouterFunction();
}
}
@@ -1,25 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.tools;
/**
* Tools for alert operations
*/
public interface AlertTools {
}
@@ -1,25 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.tools;
/**
* Tools for metrics operations
*/
public interface MetricsTools {
}
@@ -1,50 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.tools;
import org.springframework.ai.chat.model.ToolContext;
import java.util.List;
/**
* Interface for Monitoring Tools
*/
public interface MonitorTools {
String addMonitor(String name, ToolContext context);
/**
* Query monitor information with flexible filtering and pagination.
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
* pagination.
* Returns results as plain JSON.
*/
String listMonitors(
List<Long> ids,
String app,
Byte status,
String search,
String labels,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
ToolContext context);
}
@@ -1,25 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
/**
* Implementation of Alert Tools functionality
*/
public class AlertToolsImpl {
}
@@ -1,25 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
/**
* Implementation of Metrics Tools functionality
*/
public class MetricsToolsImpl {
}
@@ -1,87 +0,0 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import java.util.List;
/**
* Implementation of Monitoring Tools functionality
*/
@Slf4j
@Service
public class MonitorToolsImpl implements MonitorTools {
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
/**
* Tool to query monitor information with flexible filtering and pagination.
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
* pagination.
* Returns monitor names as string.
*/
@Override
@Tool(name = "list_monitors", returnDirect = true, description = """
Query monitor information with flexible filtering and pagination.
Supports filtering by monitor IDs, type, status, host, labels, sorting, and pagination.
Returns results as String. When no parameters are available, pass the default value as mentioned below. If the user doesn't provide any specific parameter, the default value will be used.
""")
public String listMonitors(
@ToolParam(description = "List of monitor IDs to filter (default: empty list)", required = false) List<Long> ids,
@ToolParam(description = "Monitor type, e.g., 'linux' (default: null)", required = false) String app,
@ToolParam(description = "Monitor status (0: no monitor, 1: usable, 2: disabled, 9: all) (default: null)", required = false) Byte status,
@ToolParam(description = "Fuzzy search for host or name (default: null)", required = false) String search,
@ToolParam(description = "Monitor labels, e.g., 'env:prod,instance:22' (default: null)", required = false) String labels,
@ToolParam(description = "Sort field, e.g., 'name' (default: gmtCreate)", required = false) String sort,
@ToolParam(description = "Sort order, 'asc' or 'desc' (default: desc)", required = false) String order,
@ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex,
@ToolParam(description = "Page size (default: 8)", required = false) Integer pageSize,
ToolContext context) {
try {
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
return result.getContent().stream().map(Monitor::getName).toList().toString();
} catch (Exception e) {
return "error is" + e.getMessage();
}
}
@Override
@Tool(name = "add_monitor", description = "Add a new monitor")
public String addMonitor(@ToolParam(description = "Name of the monitor") String name, ToolContext context) {
log.debug("Adding monitor with name: {}", name);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in tool: {}", subjectSum);
return "Monitor added: " + name;
}
}
+126
View File
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>hertzbeat-ai-gateway</artifactId>
<name>${project.artifactId}</name>
<description>HertzBeat Agent Gateway shell and read-only runtime boundary.</description>
<properties>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common-spring</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-base</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-manager</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-warehouse</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-observability</artifactId>
</dependency>
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-model</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,254 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.alert;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.service.AlertAnalysisPolicyService;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommandRouter;
import org.apache.hertzbeat.ai.gateway.channel.core.ChannelId;
import org.apache.hertzbeat.ai.gateway.contract.AgentAlertIncidentContext;
import org.apache.hertzbeat.ai.gateway.contract.AgentResponseLanguage;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertAnalysisPolicy;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* Groups persisted single-alert facts and routes qualifying incidents through the standard Gateway command path.
*/
@Slf4j
@Component
public class AgentAlertAnalysisEventHandler {
private static final String ALERT_ANALYSIS_PROMPT_TEMPLATE = """
Analyze this HertzBeat alert incident. Correlate the alerts, gather relevant monitoring evidence with \
tools, identify likely causes, and provide prioritized checks. Incident context:
%s""";
private final AlertAnalysisPolicyService policyService;
private final GatewayCommandRouter commandRouter;
private final Map<AnalysisWindowKey, AnalysisWindow> windows = new ConcurrentHashMap<>();
private final ExecutorService executor = new ThreadPoolExecutor(2, 2, 0, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(256), Thread.ofPlatform().name("agent-alert-analysis-", 0).factory(),
new ThreadPoolExecutor.AbortPolicy());
public AgentAlertAnalysisEventHandler(AlertAnalysisPolicyService policyService,
GatewayCommandRouter commandRouter) {
this.policyService = policyService;
this.commandRouter = commandRouter;
}
@EventListener
public void onSingleAlertCreated(SingleAlert.CreatedEvent event) {
SingleAlert alert = event.alert();
if (alert == null || !CommonConstants.ALERT_STATUS_FIRING.equals(alert.getStatus())) {
return;
}
if (alert.getId() == null || alert.getWorkspaceId() == null || alert.getWorkspaceId().isBlank()) {
return;
}
try {
executor.execute(() -> process(alert.clone()));
} catch (RuntimeException exception) {
log.warn("Agent alert analysis input was rejected for alert {}", alert.getFingerprint(), exception);
}
}
private void process(SingleAlert alert) {
for (AlertAnalysisPolicy policy : policyService.findEnabled(alert.getWorkspaceId())) {
if (alert.getWorkspaceId().equals(policy.getWorkspaceId()) && matches(policy, alert)) {
accept(policy, alert);
}
}
}
private void accept(AlertAnalysisPolicy policy, SingleAlert alert) {
String group = groupKey(policy.getGroupByLabels(), alert.getLabels());
if (group == null) {
return;
}
long now = System.currentTimeMillis();
AnalysisWindowKey windowKey = new AnalysisWindowKey(alert.getWorkspaceId(), policy.getId(), group);
AnalysisTrigger trigger;
synchronized (windows.computeIfAbsent(windowKey, ignored -> new AnalysisWindow(now))) {
AnalysisWindow window = windows.get(windowKey);
if (now - window.firstSeenAt > TimeUnit.SECONDS.toMillis(policy.getWindowSeconds())) {
window.reset(now);
}
window.alerts.put(alertKey(alert), alert.clone());
if (now - window.lastTriggeredAt < TimeUnit.SECONDS.toMillis(policy.getCooldownSeconds())
|| window.alerts.size() < policy.getMinimumAlertCount()) {
return;
}
window.lastTriggeredAt = now;
trigger = new AnalysisTrigger(alert.getWorkspaceId(), policy, group, window.firstSeenAt,
List.copyOf(window.alerts.values()), alert);
window.alerts.clear();
window.firstSeenAt = now;
}
invoke(trigger);
}
private void invoke(AnalysisTrigger trigger) {
String contextHash = GatewayText.sha256(trigger.workspaceId() + ":" + trigger.policy().getId()
+ ":" + trigger.groupKey() + ":" + trigger.firstSeenAt());
String conversationId = "alert-analysis:" + contextHash;
String commandId = "alert_" + GatewayText.sha256(conversationId + ":"
+ alertKey(trigger.triggerAlert())).substring(0, 32);
List<Long> alertIds = trigger.alerts().stream()
.map(SingleAlert::getId)
.filter(java.util.Objects::nonNull)
.distinct()
.limit(256)
.toList();
long now = System.currentTimeMillis();
UserInput userInput = UserInput.builder()
.messageId(commandId)
.conversationId(conversationId)
.target(AgentTargetRef.builder().alertId(trigger.triggerAlert().getId()).build())
.alertIncident(AgentAlertIncidentContext.builder()
.analysisPolicyId(trigger.policy().getId())
.triggerAlertId(trigger.triggerAlert().getId())
.alertIds(alertIds)
.alertCount(trigger.alerts().size())
.windowStartedAt(trigger.firstSeenAt())
.build())
.message(UserInput.Message.builder().text(prompt(trigger)).build())
.build();
InvokeCommand command = InvokeCommand.builder()
.envelope(GatewayEnvelope.builder()
.channelId(ChannelId.SYSTEM.id())
.receivedAt(now)
.actor(AgentActor.alertAnalysisActor())
.preferredLanguage(AgentResponseLanguage.systemDefault())
.workspaceId(trigger.workspaceId())
.build())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId(commandId)
.userInput(userInput)
.entryType(AgentRuntimeEntryType.ALERT_TRIGGER)
.build();
try {
commandRouter.handle(command);
} catch (RuntimeException exception) {
log.error("Automatic alert analysis failed for policy {} and conversation {}",
trigger.policy().getId(), conversationId, exception);
}
}
private String prompt(AnalysisTrigger trigger) {
List<Map<String, Object>> alerts = new ArrayList<>(trigger.alerts().size());
for (SingleAlert alert : trigger.alerts()) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("id", alert.getId());
item.put("status", alert.getStatus());
item.put("labels", alert.getLabels());
item.put("annotations", alert.getAnnotations());
item.put("content", alert.getContent());
item.put("startAt", alert.getStartAt());
item.put("activeAt", alert.getActiveAt());
item.put("triggerTimes", alert.getTriggerTimes());
alerts.add(item);
}
Map<String, Object> context = new LinkedHashMap<>();
context.put("analysisPolicyId", trigger.policy().getId());
context.put("analysisPolicyName", trigger.policy().getName());
context.put("groupByLabels", trigger.policy().getGroupByLabels());
context.put("groupKey", trigger.groupKey());
context.put("windowStartedAt", trigger.firstSeenAt());
context.put("alerts", alerts);
return ALERT_ANALYSIS_PROMPT_TEMPLATE.formatted(JsonUtil.toJson(context));
}
private boolean matches(AlertAnalysisPolicy policy, SingleAlert alert) {
Map<String, String> labels = alert.getLabels();
if (labels == null) {
return false;
}
return policy.getMatchLabels().entrySet().stream()
.allMatch(entry -> entry.getValue().equals(labels.get(entry.getKey())));
}
private String groupKey(List<String> groupByLabels, Map<String, String> labels) {
if (labels == null) {
return null;
}
List<String> components = new ArrayList<>(groupByLabels.size());
for (String label : groupByLabels) {
String value = labels.get(label);
if (value == null) {
return null;
}
components.add(label + "=" + value);
}
components.sort(Comparator.naturalOrder());
return String.join(",", components);
}
private String alertKey(SingleAlert alert) {
return alert.getId() == null ? alert.getFingerprint() : String.valueOf(alert.getId());
}
@PreDestroy
public void destroy() {
executor.shutdownNow();
}
private static final class AnalysisWindow {
private final Map<String, SingleAlert> alerts = new LinkedHashMap<>();
private long firstSeenAt;
private long lastTriggeredAt;
private AnalysisWindow(long firstSeenAt) {
this.firstSeenAt = firstSeenAt;
}
private void reset(long now) {
alerts.clear();
firstSeenAt = now;
}
}
private record AnalysisWindowKey(String workspaceId, Long policyId, String groupKey) {
}
private record AnalysisTrigger(String workspaceId, AlertAnalysisPolicy policy, String groupKey, long firstSeenAt,
List<SingleAlert> alerts, SingleAlert triggerAlert) {
}
}
@@ -0,0 +1,479 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.RunStatusPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewayStreamResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunSnapshot;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunSnapshotService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunStatus;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent.EventStatus;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEventType;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeItemKind;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeRequest;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeService;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.SignalType;
/**
* Agent command execution service. Only agent-message commands enter runtime.
*/
@Slf4j
@Service
public class AgentCommandService {
private final AgentRunAdmissionService admissionService;
private final AgentRunService runService;
private final AgentRunSnapshotService snapshotService;
private final AgentRuntimeService runtimeService;
private final GatewayRuntimeEventProjector runtimeEventProjector;
public AgentCommandService(AgentRunAdmissionService admissionService, AgentRunService runService,
AgentRunSnapshotService snapshotService,
AgentRuntimeService runtimeService,
GatewayRuntimeEventProjector runtimeEventProjector) {
this.admissionService = admissionService;
this.runService = runService;
this.snapshotService = snapshotService;
this.runtimeService = runtimeService;
this.runtimeEventProjector = runtimeEventProjector;
}
public GatewayResponse handle(InvokeCommand command) {
return invoke(command, command.userInput());
}
GatewaySingleResponse invokeFinal(GatewayCommand command, UserInput userInput) {
AgentRunAdmission admission = admit(command);
if (admission.decision() != AgentRunAdmission.Decision.EXECUTE_NEW) {
return replayFinal(command, userInput, admission);
}
AgentRuntimeRequest request = runtimeRequest((InvokeCommand) command, admission);
String conversationId = userInput.getConversationId();
AtomicReference<String> reliableFinalResult = new AtomicReference<>();
List<GatewayEvent> events = gatewayEvents(command, request, conversationId, reliableFinalResult)
.collectList()
.block();
GatewayEvent terminalEvent = terminalEvent(events);
boolean failed = terminalEvent.type() == GatewayEventType.ERROR;
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.conversationId(conversationId)
.sessionUid(request.getSession().getSessionUid())
.runUid(request.getRun().getRunUid())
.terminal(true)
.message(failed ? "error" : "completed")
.build())
.body(body(finalMessage(terminalEvent, reliableFinalResult.get()), terminalStatus(terminalEvent)))
.events(events)
.build();
}
GatewayStreamResponse invokeStream(GatewayCommand command, UserInput userInput) {
AgentRunAdmission admission = admit(command);
if (admission.decision() != AgentRunAdmission.Decision.EXECUTE_NEW) {
return replayStream(command, userInput, admission);
}
AgentRuntimeRequest request = runtimeRequest((InvokeCommand) command, admission);
String conversationId = userInput.getConversationId();
return GatewayStreamResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.conversationId(conversationId)
.sessionUid(request.getSession().getSessionUid())
.runUid(request.getRun().getRunUid())
.terminal(false)
.message("streaming")
.build())
.events(gatewayEvents(command, request, conversationId, new AtomicReference<>()))
.build();
}
AgentRuntimeRequest prepare(GatewayCommand command, UserInput userInput) {
AgentRunAdmission admission = admit(command);
if (admission.decision() != AgentRunAdmission.Decision.EXECUTE_NEW) {
throw new IllegalStateException("Agent run admission did not authorize execution");
}
return runtimeRequest((InvokeCommand) command, admission);
}
private AgentRuntimeRequest runtimeRequest(InvokeCommand command, AgentRunAdmission admission) {
return AgentRuntimeRequest.builder()
.entryType(command.entryType())
.approvalHandling(admission.approvalHandling())
.envelope(command.envelope())
.userInput(command.userInput())
.session(admission.session())
.run(admission.run())
.chatHistory(admission.chatHistory())
.build();
}
private AgentRunAdmission admit(GatewayCommand command) {
AgentRunAdmission admission = admissionService.admit((InvokeCommand) command);
if (admission.decision() == AgentRunAdmission.Decision.REJECT_MISMATCH) {
throw new IllegalArgumentException("Agent message identity conflicts with the durable run");
}
return admission;
}
/**
* Shared runtime entry after channel-specific commands have been normalized to UserInput while preserving
* command metadata and runtime entry type for response IDs and tool exposure.
*/
private GatewayResponse invoke(GatewayCommand command, UserInput userInput) {
if (command.replyMode() == ReplyMode.STREAM) {
return invokeStream(command, userInput);
}
return invokeFinal(command, userInput);
}
private Flux<GatewayEvent> gatewayEvents(GatewayCommand command, AgentRuntimeRequest request,
String conversationId, AtomicReference<String> reliableFinalResult) {
AtomicBoolean completed = new AtomicBoolean();
AssistantCompletionTracker assistantCompletion = new AssistantCompletionTracker();
AgentGatewayLifecycleRelay relay = new AgentGatewayLifecycleRelay(request.getRun().getRunUid());
Flux<GatewayEvent> lifecycle = Flux.defer(() -> runtimeService.streamInvoke(request))
.map(event -> {
GatewayEvent gatewayEvent = runtimeEventProjector.project(event, conversationId,
request.getSession().getSessionUid(), request.getRun().getRunUid());
assistantCompletion.observe(event);
try {
completeInvocationOnTerminalEvent(request.getRun(), event, completed,
assistantCompletion, reliableFinalResult);
} catch (Error error) {
if (isFatalJvmError(error)) {
completed.set(true);
relay.fail(error);
}
throw error;
}
return gatewayEvent;
})
.onErrorResume(exception -> {
if (exception instanceof Error error && isFatalJvmError(error)) {
completed.set(true);
return Flux.error(error);
}
if (exception instanceof RecoveryRequiredPersistenceException recoveryFailure) {
return Flux.just(recoveryRequiredErrorEvent(
command, request, conversationId, recoveryFailure.operatorMessage()));
}
log.debug("Agent Gateway runtime failed for run {}", request.getRun().getRunUid(), exception);
failInvocationIfIncomplete(request.getRun(), completed, "Agent Gateway runtime failed.");
return Flux.just(errorEvent(command, request, conversationId,
"Agent Gateway runtime failed."));
})
.concatWith(Flux.defer(() -> {
if (completed.get()) {
return Flux.empty();
}
String message = "Agent Gateway runtime stream completed without a terminal event.";
failInvocationIfIncomplete(request.getRun(), completed, message);
return Flux.just(errorEvent(command, request, conversationId, message));
}))
.doFinally(signalType -> completeInvocationIfStreamFinishedWithoutTerminal(request.getRun(),
signalType, completed));
return relay.connect(lifecycle);
}
private void completeInvocationOnTerminalEvent(AgentRun run, AgentRuntimeEvent event,
AtomicBoolean completed, AssistantCompletionTracker assistantCompletion,
AtomicReference<String> reliableFinalResult) {
if (completed.get()) {
return;
}
if (event.getType() != AgentRuntimeEventType.RUN_COMPLETED
&& event.getType() != AgentRuntimeEventType.ERROR) {
return;
}
if (!completed.compareAndSet(false, true)) {
return;
}
String terminalMessage = StringUtils.hasText(event.getErrorMessage())
? event.getErrorMessage()
: "Agent Gateway runtime failed.";
try {
if (event.getType() == AgentRuntimeEventType.RUN_COMPLETED) {
if (!assistantCompletion.latestAssistantCompleted()) {
throw new IllegalStateException("Agent runtime completed without an assistant message completion");
}
if (!StringUtils.hasText(event.getResult())) {
throw new IllegalStateException("Agent runtime completed without a durable result");
}
AgentRun succeededRun = runService.markSucceeded(run, event.getResult());
if (succeededRun == null || !StringUtils.hasText(succeededRun.getResultSummary())) {
throw new IllegalStateException("Succeeded agent run must expose its persisted result");
}
reliableFinalResult.compareAndSet(null, succeededRun.getResultSummary());
return;
}
if (event.getStatus() == EventStatus.RECOVERY_REQUIRED) {
runService.markRecoveryRequired(run, terminalMessage);
} else {
runService.markFailed(run, terminalMessage);
}
} catch (RuntimeException exception) {
if (event.getStatus() == EventStatus.RECOVERY_REQUIRED) {
throw new RecoveryRequiredPersistenceException(terminalMessage, exception);
}
completed.set(false);
throw exception;
} catch (Error error) {
if (event.getStatus() == EventStatus.RECOVERY_REQUIRED && !isFatalJvmError(error)) {
throw new RecoveryRequiredPersistenceException(terminalMessage, error);
}
completed.set(false);
throw error;
}
}
private boolean isFatalJvmError(Error error) {
return error instanceof VirtualMachineError
|| error instanceof ThreadDeath
|| error instanceof LinkageError;
}
private void failInvocationIfIncomplete(AgentRun run, AtomicBoolean completed, String message) {
if (!completed.compareAndSet(false, true)) {
return;
}
try {
runService.markFailed(run, message);
} catch (RuntimeException exception) {
completed.set(false);
throw exception;
}
}
private void completeInvocationIfStreamFinishedWithoutTerminal(AgentRun run, SignalType signalType,
AtomicBoolean completed) {
if (!completed.compareAndSet(false, true)) {
return;
}
if (signalType == SignalType.CANCEL) {
runService.markCancelled(run, "Runtime stream client disconnected.");
return;
}
if (signalType == SignalType.ON_ERROR) {
runService.markFailed(run, "Agent Gateway runtime failed.");
return;
}
runService.markFailed(run, "Agent Gateway runtime stream completed without a terminal event.");
}
private GatewayEvent terminalEvent(List<GatewayEvent> events) {
for (int index = events.size() - 1; index >= 0; index--) {
GatewayEvent event = events.get(index);
if (isTerminal(event)) {
return event;
}
}
throw new IllegalStateException("Mapped runtime events must contain a terminal event");
}
private String finalMessage(GatewayEvent terminalEvent, String reliableFinalResult) {
if (terminalEvent.type() == GatewayEventType.ERROR) {
if (terminalEvent.payload() instanceof ErrorPayload payload
&& StringUtils.hasText(payload.errorMessage())) {
return payload.errorMessage();
}
return "Agent Gateway runtime failed.";
}
if (!StringUtils.hasText(reliableFinalResult)) {
throw new IllegalStateException("Completed agent run must expose its reliable final result");
}
return reliableFinalResult;
}
private String terminalStatus(GatewayEvent terminalEvent) {
if (terminalEvent.type() != GatewayEventType.ERROR) {
return AgentRunStatus.SUCCEEDED.name();
}
if (terminalEvent.payload() instanceof ErrorPayload payload
&& EventStatus.RECOVERY_REQUIRED.externalName().equals(payload.status())) {
return AgentRunStatus.RECOVERY_REQUIRED.name();
}
return AgentRunStatus.FAILED.name();
}
private boolean isTerminal(GatewayEvent event) {
return event.type() == GatewayEventType.RUN_COMPLETED
|| event.type() == GatewayEventType.RUN_STATUS
|| event.type() == GatewayEventType.ERROR;
}
private GatewayStreamResponse replayStream(GatewayCommand command, UserInput userInput,
AgentRunAdmission admission) {
AgentRunSnapshot snapshot = snapshotService.snapshot(admission.session(), admission.run());
boolean terminal = admission.decision() == AgentRunAdmission.Decision.REPLAY_TERMINAL;
return GatewayStreamResponse.builder()
.meta(replayMeta(command, userInput, snapshot, terminal))
.events(Flux.just(runStatusEvent(command, userInput, snapshot)))
.build();
}
private GatewaySingleResponse replayFinal(GatewayCommand command, UserInput userInput,
AgentRunAdmission admission) {
AgentRunSnapshot snapshot = snapshotService.snapshot(admission.session(), admission.run());
boolean terminal = admission.decision() == AgentRunAdmission.Decision.REPLAY_TERMINAL;
GatewayEvent event = runStatusEvent(command, userInput, snapshot);
String message = StringUtils.hasText(snapshot.result()) ? snapshot.result() : snapshot.errorMessage();
return GatewaySingleResponse.builder()
.meta(replayMeta(command, userInput, snapshot, terminal))
.body(body(message, snapshot.status()))
.events(List.of(event))
.build();
}
private Meta replayMeta(GatewayCommand command, UserInput userInput, AgentRunSnapshot snapshot,
boolean terminal) {
return Meta.builder()
.commandId(command.commandId())
.conversationId(userInput.getConversationId())
.sessionUid(snapshot.sessionUid())
.runUid(snapshot.runUid())
.terminal(terminal)
.message(terminal ? "replayed" : "running")
.build();
}
private GatewayEvent runStatusEvent(GatewayCommand command, UserInput userInput, AgentRunSnapshot snapshot) {
return GatewayEvent.builder()
.type(GatewayEventType.RUN_STATUS)
.eventId(snapshot.runUid() + ":status:" + snapshot.status().toLowerCase(java.util.Locale.ROOT))
.conversationId(userInput.getConversationId())
.sessionUid(snapshot.sessionUid())
.runUid(snapshot.runUid())
.payload(RunStatusPayload.builder()
.status(snapshot.status())
.result(snapshot.result())
.errorMessage(snapshot.errorMessage())
.replayAvailable(snapshot.replayAvailable())
.build())
.timestamp(System.currentTimeMillis())
.build();
}
private GatewayEvent errorEvent(GatewayCommand command, AgentRuntimeRequest request, String conversationId,
String message) {
return GatewayEvent.builder()
.type(GatewayEventType.ERROR)
.eventId(command.commandId() + ":error")
.conversationId(conversationId)
.sessionUid(request.getSession().getSessionUid())
.runUid(request.getRun().getRunUid())
.payload(ErrorPayload.builder()
.errorMessage(message)
.build())
.timestamp(System.currentTimeMillis())
.build();
}
private GatewayEvent recoveryRequiredErrorEvent(GatewayCommand command, AgentRuntimeRequest request,
String conversationId, String message) {
return GatewayEvent.builder()
.type(GatewayEventType.ERROR)
.eventId(command.commandId() + ":recovery-required")
.conversationId(conversationId)
.sessionUid(request.getSession().getSessionUid())
.runUid(request.getRun().getRunUid())
.payload(ErrorPayload.builder()
.errorMessage(message)
.status(EventStatus.RECOVERY_REQUIRED.externalName())
.build())
.timestamp(System.currentTimeMillis())
.build();
}
private static final class RecoveryRequiredPersistenceException extends RuntimeException {
private final String operatorMessage;
private RecoveryRequiredPersistenceException(String operatorMessage, Throwable cause) {
super("Agent recovery-required state could not be persisted", cause);
this.operatorMessage = operatorMessage;
}
private String operatorMessage() {
return operatorMessage;
}
}
private static final class AssistantCompletionTracker {
private String latestAssistantItemId;
private String completedAssistantItemId;
private void observe(AgentRuntimeEvent event) {
if (event.getItemKind() == AgentRuntimeItemKind.TOOL_CALL) {
invalidate();
return;
}
if (event.getItemKind() != AgentRuntimeItemKind.ASSISTANT_MESSAGE) {
return;
}
if (event.getType() == AgentRuntimeEventType.ITEM_STARTED) {
latestAssistantItemId = event.getItemId();
completedAssistantItemId = null;
} else if (event.getType() == AgentRuntimeEventType.ITEM_DELTA
&& !Objects.equals(latestAssistantItemId, event.getItemId())) {
invalidate();
} else if (event.getType() == AgentRuntimeEventType.ITEM_COMPLETED) {
completedAssistantItemId = Objects.equals(latestAssistantItemId, event.getItemId())
? event.getItemId()
: null;
}
}
private void invalidate() {
latestAssistantItemId = null;
completedAssistantItemId = null;
}
private boolean latestAssistantCompleted() {
return StringUtils.hasText(latestAssistantItemId)
&& Objects.equals(latestAssistantItemId, completedAssistantItemId);
}
}
private Map<String, Object> body(String message, String status) {
return Map.of("message", StringUtils.hasText(message) ? message : "",
"status", StringUtils.hasText(status) ? status : "");
}
}
@@ -0,0 +1,168 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
import org.apache.hertzbeat.manager.service.entity.EntityWorkspaceQueryService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves and rechecks persisted authority for an exact workspace-owned Entity. */
@Service
public class AgentEntityTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "entity.";
public static final String AUTHORITY_VERSION_PREFIX = "entity-authority.";
public static final String TARGET_VERSION = "entity.v1";
public static final String AUTHORITY_VERSION = "entity-authority.v1";
private final EntityWorkspaceQueryService entityWorkspaceQueryService;
public AgentEntityTargetAuthorityService(EntityWorkspaceQueryService entityWorkspaceQueryService) {
this.entityWorkspaceQueryService = entityWorkspaceQueryService;
}
public AgentTargetRef canonicalize(String workspaceId, long entityId) {
if (!StringUtils.hasText(workspaceId) || entityId <= 0) {
throw unavailable();
}
ObserveEntity entity;
try {
entity = entityWorkspaceQueryService.findEntityById(workspaceId, entityId)
.filter(candidate -> Objects.equals(workspaceId, candidate.getWorkspaceId()))
.filter(candidate -> Objects.equals(entityId, candidate.getId()))
.orElseThrow(this::unavailable);
} catch (UnavailableException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.entityId(entityId)
.authority(AgentTargetAuthority.builder()
.bindingId(entityId)
.version(AUTHORITY_VERSION)
.hash(authorityHash(workspaceId, entity))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return Objects.equals(target, canonicalize(workspaceId, target.getEntityId()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& TARGET_VERSION.equals(target.getVersion())
&& target.getEntityId() != null && target.getEntityId() > 0
&& authority != null
&& Objects.equals(target.getEntityId(), authority.getBindingId())
&& AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getAlertId() == null
&& target.getAlertType() == null && target.getCollector() == null
&& target.getSignal() == null && target.getTopology() == null
&& target.getTrace() == null && target.getLog() == null && target.getService() == null;
}
private String authorityHash(String workspaceId, ObserveEntity entity) {
StringBuilder material = new StringBuilder("entity-authority.v1;");
append(material, "workspace", workspaceId);
append(material, "id", entity.getId());
append(material, "type", entity.getType());
append(material, "name", entity.getName());
append(material, "displayName", entity.getDisplayName());
append(material, "subtype", entity.getSubtype());
append(material, "namespace", entity.getNamespace());
append(material, "environment", entity.getEnvironment());
append(material, "status", entity.getStatus());
append(material, "criticality", entity.getCriticality());
append(material, "owner", entity.getOwner());
append(material, "lifecycle", entity.getLifecycle());
append(material, "tier", entity.getTier());
append(material, "system", entity.getSystem());
append(material, "source", entity.getSource());
append(material, "description", entity.getDescription());
appendMap(material, "labels", entity.getLabels());
appendList(material, "tags", entity.getTags());
append(material, "updatedAt", entity.getGmtUpdate());
try {
return "sha256:" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(material.toString().getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required for entity target authority", exception);
}
}
private void appendMap(StringBuilder material, String field, Map<String, String> values) {
append(material, field + "Size", values == null ? null : values.size());
if (values != null) {
new TreeMap<>(values).forEach((key, value) -> {
append(material, field + "Key", key);
append(material, field + "Value", value);
});
}
}
private void appendList(StringBuilder material, String field, List<String> values) {
append(material, field + "Size", values == null ? null : values.size());
if (values != null) {
values.forEach(value -> append(material, field + "Value", value));
}
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : String.valueOf(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Entity target is unavailable");
}
}
}
@@ -0,0 +1,139 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
import reactor.util.concurrent.Queues;
/**
* Separates the durable runtime subscription from optional client demand.
*/
@Slf4j
final class AgentGatewayLifecycleRelay {
private static final int LIVE_BUFFER_SIZE = Queues.SMALL_BUFFER_SIZE;
private static final String LIFECYCLE_FAILURE = "Agent Gateway lifecycle failed";
private final String runUid;
private final Sinks.One<GatewayEvent> started = Sinks.one();
private final Sinks.Many<GatewayEvent> live = Sinks.many().multicast()
.onBackpressureBuffer(LIVE_BUFFER_SIZE, false);
private final Sinks.One<GatewayEvent> terminal = Sinks.one();
private final AtomicBoolean primaryClient = new AtomicBoolean();
private final AtomicBoolean drainStarted = new AtomicBoolean();
private final AtomicBoolean startSeen = new AtomicBoolean();
private final AtomicBoolean terminalSeen = new AtomicBoolean();
private final AtomicLong droppedLiveEvents = new AtomicLong();
AgentGatewayLifecycleRelay(String runUid) {
this.runUid = runUid;
}
Flux<GatewayEvent> connect(Flux<GatewayEvent> lifecycle) {
return Flux.defer(() -> {
Flux<GatewayEvent> clientEvents = primaryClient.compareAndSet(false, true)
? Flux.concat(started.asMono(), live.asFlux(), terminal.asMono())
: terminal.asMono().flux();
return clientEvents.doOnSubscribe(ignored -> startDrain(lifecycle));
});
}
void fail(Throwable failure) {
completeStartIfMissing();
completeLive();
Throwable clientFailure = isFatal(failure)
? failure
: new IllegalStateException(LIFECYCLE_FAILURE);
emitCritical("terminal failure", terminal.tryEmitError(clientFailure));
}
private void startDrain(Flux<GatewayEvent> lifecycle) {
if (!drainStarted.compareAndSet(false, true)) {
return;
}
try {
lifecycle.subscribe(this::emit, this::fail, this::complete);
} catch (RuntimeException failure) {
fail(failure);
throw failure;
} catch (Error failure) {
fail(failure);
throw failure;
}
}
private void emit(GatewayEvent event) {
if (event.type() == GatewayEvent.GatewayEventType.RUN_STARTED) {
startSeen.set(true);
emitCritical("run start", started.tryEmitValue(event));
return;
}
if (event.type() == GatewayEvent.GatewayEventType.RUN_COMPLETED
|| event.type() == GatewayEvent.GatewayEventType.ERROR) {
terminalSeen.set(true);
emitCritical("terminal event", terminal.tryEmitValue(event));
return;
}
Sinks.EmitResult result = live.tryEmitNext(event);
if (result.isFailure()) {
long dropped = droppedLiveEvents.incrementAndGet();
if (dropped == 1) {
log.warn("Agent Gateway live event buffer is full for run {}; dropping nonterminal events", runUid);
}
}
}
private void complete() {
completeStartIfMissing();
completeLive();
if (!terminalSeen.get()) {
emitCritical("missing terminal event", terminal.tryEmitError(
new IllegalStateException(LIFECYCLE_FAILURE)));
}
long dropped = droppedLiveEvents.get();
if (dropped > 1) {
log.warn("Agent Gateway dropped {} nonterminal live events for run {}", dropped, runUid);
}
}
private void completeStartIfMissing() {
if (!startSeen.get()) {
emitCritical("empty run start", started.tryEmitEmpty());
}
}
private void completeLive() {
emitCritical("live completion", live.tryEmitComplete());
}
private void emitCritical(String signal, Sinks.EmitResult result) {
if (result.isFailure()) {
log.warn("Agent Gateway could not emit {} for run {}: {}", signal, runUid, result);
}
}
private boolean isFatal(Throwable failure) {
return failure instanceof VirtualMachineError
|| failure instanceof ThreadDeath
|| failure instanceof LinkageError;
}
}
@@ -0,0 +1,228 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.apache.hertzbeat.ai.gateway.contract.AgentLogRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.observability.logs.service.LogQueryService;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves one exact non-live Log Explore page through the trusted-workspace query boundary. */
@Service
public class AgentLogTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "log-page.";
public static final String AUTHORITY_VERSION_PREFIX = "log-page-authority.";
public static final String TARGET_VERSION = "log-page.v1";
public static final String AUTHORITY_VERSION = "log-page-authority.v1";
private static final long MAX_RANGE_MILLIS = Duration.ofDays(7).toMillis();
private static final Pattern SAFE_ID = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._:-]{0,127}");
private static final Set<String> SEVERITIES = Set.of("TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL");
private final LogQueryService logQueryService;
public AgentLogTargetAuthorityService(LogQueryService logQueryService) {
this.logQueryService = logQueryService;
}
public AgentTargetRef canonicalize(String workspaceId, AgentLogRef source) {
AgentLogRef log = normalizeSource(source);
Page<LogEntry> page;
try {
page = query(workspaceId, log);
} catch (RuntimeException ignored) {
throw unavailable();
}
if (page == null || page.isEmpty() || page.getNumber() != log.getPageIndex()
|| page.getSize() != log.getPageSize()) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.log(log)
.authority(AgentTargetAuthority.builder()
.version(AUTHORITY_VERSION)
.hash("sha256:" + GatewayText.sha256(authorityMaterial(workspaceId, log, page)))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return Objects.equals(target, canonicalize(workspaceId, target.getLog()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
if (target == null || !TARGET_VERSION.equals(target.getVersion()) || target.getLog() == null
|| authority == null || authority.getBindingId() != null
|| !AUTHORITY_VERSION.equals(authority.getVersion())
|| authority.getHash() == null || !authority.getHash().matches("sha256:[0-9a-f]{64}")
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getEntityId() != null || target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getTrace() != null || target.getService() != null) {
return false;
}
try {
return Objects.equals(target.getLog(), normalizeSource(target.getLog()));
} catch (UnavailableException ignored) {
return false;
}
}
public AgentLogRef normalizeSource(AgentLogRef source) {
if (source == null || source.getStart() == null || source.getEnd() == null || source.getStart() <= 0
|| source.getEnd() <= source.getStart() || source.getEnd() - source.getStart() > MAX_RANGE_MILLIS
|| !optionalSafeId(source.getTraceId()) || !optionalSafeId(source.getSpanId())
|| source.getSeverityNumber() != null
&& (source.getSeverityNumber() < 1 || source.getSeverityNumber() > 24)
|| source.getHideInternal() == null || source.getHideNoise() == null
|| source.getPageIndex() == null || source.getPageIndex() < 0 || source.getPageIndex() > 10_000
|| source.getPageSize() == null || source.getPageSize() < 1 || source.getPageSize() > 100) {
throw unavailable();
}
String severity = text(source.getSeverityText(), 16);
if (severity != null) {
severity = severity.toUpperCase(Locale.ROOT);
if (!SEVERITIES.contains(severity)) {
throw unavailable();
}
}
return AgentLogRef.builder()
.start(source.getStart()).end(source.getEnd())
.traceId(source.getTraceId()).spanId(source.getSpanId())
.severityNumber(source.getSeverityNumber()).severityText(severity)
.search(text(source.getSearch(), 256))
.serviceName(text(source.getServiceName(), 512))
.serviceNamespace(text(source.getServiceNamespace(), 512))
.environment(text(source.getEnvironment(), 512))
.resourceFilter(text(source.getResourceFilter(), 2048))
.attributeFilter(text(source.getAttributeFilter(), 2048))
.hideInternal(source.getHideInternal()).hideNoise(source.getHideNoise())
.pageIndex(source.getPageIndex()).pageSize(source.getPageSize())
.build();
}
private Page<LogEntry> query(String workspaceId, AgentLogRef log) {
return logQueryService.list(workspaceId, null, log.getStart(), log.getEnd(), log.getTraceId(), log.getSpanId(),
log.getSeverityNumber(), log.getSeverityText(), log.getSearch(), log.getServiceName(),
log.getServiceNamespace(), log.getEnvironment(), log.getResourceFilter(), log.getAttributeFilter(),
log.getPageIndex(), log.getPageSize(), log.getHideInternal(), log.getHideNoise());
}
private String authorityMaterial(String workspaceId, AgentLogRef log, Page<LogEntry> page) {
StringBuilder material = new StringBuilder("log-page-authority.v1;");
append(material, "workspaceId", workspaceId);
append(material, "scope", JsonUtil.toJson(log));
append(material, "number", page.getNumber());
append(material, "size", page.getSize());
append(material, "totalElements", page.getTotalElements());
append(material, "totalPages", page.getTotalPages());
for (LogEntry row : page.getContent()) {
append(material, "timeUnixNano", row == null ? null : row.getTimeUnixNano());
append(material, "observedTimeUnixNano", row == null ? null : row.getObservedTimeUnixNano());
append(material, "severityNumber", row == null ? null : row.getSeverityNumber());
append(material, "severityText", row == null ? null : row.getSeverityText());
append(material, "body", row == null ? null : canonical(row.getBody()));
append(material, "attributes", row == null ? null : canonical(row.getAttributes()));
append(material, "droppedAttributesCount", row == null ? null : row.getDroppedAttributesCount());
append(material, "traceId", row == null ? null : row.getTraceId());
append(material, "spanId", row == null ? null : row.getSpanId());
append(material, "traceFlags", row == null ? null : row.getTraceFlags());
append(material, "resource", row == null ? null : canonical(row.getResource()));
append(material, "resourceSchemaUrl", row == null ? null : row.getResourceSchemaUrl());
append(material, "instrumentationScope", row == null ? null : canonical(row.getInstrumentationScope()));
append(material, "scopeSchemaUrl", row == null ? null : row.getScopeSchemaUrl());
}
return material.toString();
}
private Object canonical(Object value) {
if (value instanceof Map<?, ?> map) {
Map<String, Object> sorted = new TreeMap<>();
map.forEach((key, nested) -> sorted.put(String.valueOf(key), canonical(nested)));
return sorted;
}
if (value instanceof Iterable<?> iterable) {
List<Object> values = new ArrayList<>();
iterable.forEach(nested -> values.add(canonical(nested)));
return values;
}
return value;
}
private String text(String value, int maximumLength) {
if (value == null) {
return null;
}
String normalized = value.trim();
if (!StringUtils.hasText(normalized) || normalized.length() > maximumLength
|| normalized.codePoints().anyMatch(code -> code < 32 || code == 127)
|| !Objects.equals(normalized, GatewayText.redactSecrets(normalized))) {
throw unavailable();
}
return normalized;
}
private boolean optionalSafeId(String value) {
return value == null || SAFE_ID.matcher(value).matches();
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : value instanceof String ? (String) value : JsonUtil.toJson(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Log target is unavailable");
}
}
}
@@ -0,0 +1,116 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.FailureKind;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.TargetCanonicalizationException;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Isolates strict source/canonical adaptation for one exact non-live Log Explore page. */
@Service
public class AgentLogTargetCanonicalizationAdapter {
private final AgentLogTargetAuthorityService authorityService;
public AgentLogTargetCanonicalizationAdapter(AgentLogTargetAuthorityService authorityService) {
this.authorityService = authorityService;
}
public boolean isIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getLog() != null
|| hasVersion(target.getVersion(), AgentLogTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasVersion(
authority.getVersion(), AgentLogTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
public GatewayCommand.InvokeCommand canonicalize(GatewayCommand.InvokeCommand command) {
AgentTargetRef source = requireSourceIntent(command.userInput().getTarget());
try {
AgentTargetRef target = authorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getLog());
if (!authorityService.isCanonicalTarget(target)
|| !Objects.equals(authorityService.normalizeSource(source.getLog()), target.getLog())) {
throw unavailable();
}
return withTarget(command, target);
} catch (AgentLogTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (TargetCanonicalizationException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw unavailable();
}
}
public GatewayCommand.InvokeCommand replayCommand(
GatewayCommand.InvokeCommand command, AgentTargetRef persistedTarget) {
AgentTargetRef source = requireSourceIntent(command.userInput().getTarget());
try {
if (!authorityService.isCanonicalTarget(persistedTarget)
|| !Objects.equals(authorityService.normalizeSource(source.getLog()), persistedTarget.getLog())) {
throw mismatch();
}
return withTarget(command, persistedTarget);
} catch (TargetCanonicalizationException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw mismatch();
}
}
public static AgentTargetRef sourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder().log(target.getLog()).build();
}
private AgentTargetRef requireSourceIntent(AgentTargetRef target) {
if (target == null || target.getLog() == null || target.getVersion() != null || target.getAuthority() != null
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getEntityId() != null || target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getTrace() != null || target.getService() != null) {
throw unavailable();
}
return target;
}
private GatewayCommand.InvokeCommand withTarget(GatewayCommand.InvokeCommand command, AgentTargetRef target) {
UserInput userInput = command.userInput().toBuilder().target(target).build();
return new GatewayCommand.InvokeCommand(command.envelope(), command.replyMode(), command.commandId(), userInput,
command.entryType());
}
private boolean hasVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private TargetCanonicalizationException mismatch() {
return new TargetCanonicalizationException(FailureKind.MISMATCH);
}
private TargetCanonicalizationException unavailable() {
return new TargetCanonicalizationException(FailureKind.UNAVAILABLE);
}
}
@@ -0,0 +1,52 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.List;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.runtime.AgentApprovalHandling;
import org.apache.hertzbeat.ai.gateway.runtime.TranscriptMessage;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
/**
* Linearized outcome of admitting one durable Agent Gateway message.
*/
public record AgentRunAdmission(
Decision decision,
AgentSession session,
AgentRun run,
AgentApprovalHandling approvalHandling,
List<TranscriptMessage> chatHistory) {
public AgentRunAdmission {
Objects.requireNonNull(decision, "Agent run admission decision is required");
Objects.requireNonNull(session, "Agent run admission session is required");
Objects.requireNonNull(run, "Agent run admission run is required");
Objects.requireNonNull(approvalHandling, "Agent run admission approval handling is required");
chatHistory = chatHistory == null ? List.of() : List.copyOf(chatHistory);
}
/** Durable admission decisions for one normalized message identity. */
public enum Decision {
EXECUTE_NEW,
REPLAY_ACTIVE,
REPLAY_TERMINAL,
REJECT_MISMATCH
}
}
@@ -0,0 +1,90 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.Objects;
import java.util.Optional;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentRunDao;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionService;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves the canonical session before entering the serialized admission transaction. */
@Service
public class AgentRunAdmissionService {
private final AgentSessionService sessionService;
private final AgentRunAdmissionTransaction admissionTransaction;
private final AgentRunDao runDao;
private final AgentTargetCanonicalizationService targetCanonicalizationService;
public AgentRunAdmissionService(AgentSessionService sessionService,
AgentRunAdmissionTransaction admissionTransaction, AgentRunDao runDao,
AgentTargetCanonicalizationService targetCanonicalizationService) {
this.sessionService = sessionService;
this.admissionTransaction = admissionTransaction;
this.runDao = runDao;
this.targetCanonicalizationService = targetCanonicalizationService;
}
public AgentRunAdmission admit(InvokeCommand command) {
validateCommandIdentity(command);
if (!targetCanonicalizationService.requiresCanonicalization(command)) {
return admitResolved(command, command);
}
Optional<AgentSession> existingSession = sessionService.findSession(
command.envelope(), command.userInput().getConversationId());
if (hasExistingRun(existingSession, command)) {
return admissionTransaction.admit(existingSession.orElseThrow().getId(), command, null);
}
try {
InvokeCommand canonicalCommand = targetCanonicalizationService.canonicalize(command);
return admitResolved(command, canonicalCommand);
} catch (RuntimeException failure) {
Optional<AgentSession> winnerSession = sessionService.findSession(
command.envelope(), command.userInput().getConversationId());
if (hasExistingRun(winnerSession, command)) {
return admissionTransaction.admit(winnerSession.orElseThrow().getId(), command, null);
}
throw failure;
}
}
private AgentRunAdmission admitResolved(InvokeCommand sourceCommand, InvokeCommand executionCommand) {
AgentSession session = sessionService.findOrCreateSession(
sourceCommand.envelope(), sourceCommand.userInput(), sourceCommand.entryType());
if (!Objects.equals(session.getWorkspaceId(), sourceCommand.envelope().getWorkspaceId())) {
throw new IllegalArgumentException("Agent run session workspace does not match the command");
}
return admissionTransaction.admit(session.getId(), sourceCommand, executionCommand);
}
private boolean hasExistingRun(Optional<AgentSession> session, InvokeCommand command) {
return session.isPresent() && runDao.findBySessionIdAndMessageId(
session.get().getId(), command.userInput().getMessageId()).isPresent();
}
private void validateCommandIdentity(InvokeCommand command) {
if (!StringUtils.hasText(command.userInput().getMessageId())
|| !Objects.equals(command.commandId(), command.userInput().getMessageId())) {
throw new IllegalArgumentException("Agent command and message identity must match");
}
}
}
@@ -0,0 +1,200 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.apache.hertzbeat.ai.gateway.application.AgentRunAdmission.Decision;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunStatus;
import org.apache.hertzbeat.ai.gateway.conversation.AgentTranscriptRecorder;
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentRunDao;
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentSessionDao;
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
import org.apache.hertzbeat.ai.gateway.runtime.AgentApprovalHandling;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.runtime.TranscriptMessage;
import org.apache.hertzbeat.ai.gateway.contract.AgentRunRequestSnapshot;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
/** Serializes run admission after the canonical session has been resolved. */
@Service
public class AgentRunAdmissionTransaction {
private final AgentSessionDao sessionDao;
private final AgentRunDao runDao;
private final AgentRunService runService;
private final AgentTranscriptRecorder transcriptRecorder;
private final AgentTargetCanonicalizationService targetCanonicalizationService;
public AgentRunAdmissionTransaction(AgentSessionDao sessionDao, AgentRunDao runDao,
AgentRunService runService, AgentTranscriptRecorder transcriptRecorder,
AgentTargetCanonicalizationService targetCanonicalizationService) {
this.sessionDao = sessionDao;
this.runDao = runDao;
this.runService = runService;
this.transcriptRecorder = transcriptRecorder;
this.targetCanonicalizationService = targetCanonicalizationService;
}
@Transactional
public AgentRunAdmission admit(Long sessionId, InvokeCommand command) {
return admit(sessionId, command, command);
}
@Transactional
public AgentRunAdmission admit(Long sessionId, InvokeCommand sourceCommand, InvokeCommand executionCommand) {
AgentSession session = sessionDao.findFirstById(sessionId)
.orElseThrow(() -> new IllegalStateException("Agent run session disappeared during admission"));
validateSessionIdentity(session, sourceCommand);
AgentApprovalHandling approvalHandling = approvalHandling(sourceCommand.replyMode());
Optional<AgentRun> existing = runDao.findBySessionIdAndMessageId(
session.getId(), sourceCommand.userInput().getMessageId());
if (existing.isPresent()) {
InvokeCommand replayCommand = replayCommand(sourceCommand, existing.get(), executionCommand);
if (replayCommand == null) {
return admission(Decision.REJECT_MISMATCH, session, existing.get(), approvalHandling);
}
String fingerprint = AgentRunRequestFingerprint.from(replayCommand, approvalHandling);
return admitExisting(session, existing.get(), replayCommand, approvalHandling, fingerprint);
}
if (executionCommand == null) {
throw new IllegalStateException("Canonical target is required for a new Agent run");
}
AgentRunRequestSnapshot requestSnapshot = AgentRunRequestFingerprint.snapshot(
executionCommand, approvalHandling);
String fingerprint = AgentRunRequestFingerprint.from(requestSnapshot);
return createAndAdmit(session, executionCommand, approvalHandling, fingerprint, requestSnapshot);
}
private InvokeCommand replayCommand(InvokeCommand sourceCommand, AgentRun run, InvokeCommand executionCommand) {
if (!targetCanonicalizationService.requiresCanonicalization(sourceCommand)) {
return executionCommand == null ? sourceCommand : executionCommand;
}
try {
return targetCanonicalizationService.replayCommand(sourceCommand, AgentRunService.targetFromRun(run));
} catch (IllegalArgumentException ignored) {
return null;
}
}
private AgentRunAdmission createAndAdmit(AgentSession session, InvokeCommand command,
AgentApprovalHandling approvalHandling, String fingerprint,
AgentRunRequestSnapshot requestSnapshot) {
List<TranscriptMessage> history = transcriptRecorder.chatHistory(session.getId());
AgentRun run = runService.createOrResumeRun(session, command.userInput(), command.entryType());
transcriptRecorder.recordUserTranscriptEntry(session, run, command.userInput(),
AgentRunRequestFingerprint.VERSION, fingerprint, requestSnapshot);
AgentRun running = runService.markRunning(run);
return new AgentRunAdmission(Decision.EXECUTE_NEW, session, running, approvalHandling, history);
}
private AgentRunAdmission admitExisting(AgentSession session, AgentRun run, InvokeCommand command,
AgentApprovalHandling approvalHandling, String fingerprint) {
if (!sameRunIdentity(session, run, command)) {
return admission(Decision.REJECT_MISMATCH, session, run, approvalHandling);
}
Optional<TranscriptMessage> requestMessage = transcriptRecorder.findRunRequestMessage(run.getId());
if (requestMessage.isEmpty()) {
if (isScheduleReservation(run, command)) {
return admitScheduleReservation(session, run, command, approvalHandling, fingerprint);
}
return admission(Decision.REJECT_MISMATCH, session, run, approvalHandling);
}
if (!matchesFingerprint(requestMessage.get(), fingerprint)) {
return admission(Decision.REJECT_MISMATCH, session, run, approvalHandling);
}
AgentRunStatus status = status(run);
if (status == AgentRunStatus.RUNNING) {
return admission(Decision.REPLAY_ACTIVE, session, run, approvalHandling);
}
if (status == AgentRunStatus.SUCCEEDED
|| status == AgentRunStatus.FAILED
|| status == AgentRunStatus.CANCELLED
|| status == AgentRunStatus.RECOVERY_REQUIRED) {
return admission(Decision.REPLAY_TERMINAL, session, run, approvalHandling);
}
return admission(Decision.REJECT_MISMATCH, session, run, approvalHandling);
}
private AgentRunAdmission admitScheduleReservation(AgentSession session, AgentRun run, InvokeCommand command,
AgentApprovalHandling approvalHandling, String fingerprint) {
List<TranscriptMessage> history = transcriptRecorder.chatHistory(session.getId());
transcriptRecorder.recordUserTranscriptEntry(session, run, command.userInput(),
AgentRunRequestFingerprint.VERSION, fingerprint);
AgentRun running = runService.markRunning(run);
return new AgentRunAdmission(Decision.EXECUTE_NEW, session, running, approvalHandling, history);
}
private boolean matchesFingerprint(TranscriptMessage requestMessage, String fingerprint) {
return Objects.equals(AgentRunRequestFingerprint.VERSION, requestMessage.getRequestFingerprintVersion())
&& Objects.equals(fingerprint, requestMessage.getRequestFingerprint());
}
private boolean sameRunIdentity(AgentSession session, AgentRun run, InvokeCommand command) {
return Objects.equals(session.getId(), run.getSessionId())
&& Objects.equals(command.userInput().getMessageId(), run.getMessageId())
&& Objects.equals(command.entryType().name(), run.getEntryType());
}
private boolean isScheduleReservation(AgentRun run, InvokeCommand command) {
return status(run) == AgentRunStatus.CREATED
&& command.entryType() == AgentRuntimeEntryType.SCHEDULE_TRIGGER
&& Objects.equals(AgentRuntimeEntryType.SCHEDULE_TRIGGER.name(), run.getEntryType())
&& Objects.equals(AgentRunService.targetFromRun(run), command.userInput().getTarget());
}
private AgentRunAdmission admission(Decision decision, AgentSession session, AgentRun run,
AgentApprovalHandling approvalHandling) {
return new AgentRunAdmission(decision, session, run, approvalHandling, List.of());
}
private AgentRunStatus status(AgentRun run) {
if (!StringUtils.hasText(run.getStatus())) {
throw new IllegalStateException("Agent run status is required during admission");
}
return AgentRunStatus.valueOf(run.getStatus());
}
private AgentApprovalHandling approvalHandling(ReplyMode replyMode) {
return replyMode == ReplyMode.STREAM
? AgentApprovalHandling.WAIT_FOR_DECISION
: AgentApprovalHandling.DENY;
}
private void validateSessionIdentity(AgentSession session, InvokeCommand command) {
AgentActor actor = command.envelope().getActor();
boolean matches = actor != null
&& Objects.equals(session.getWorkspaceId(), command.envelope().getWorkspaceId())
&& Objects.equals(session.getChannel(), command.envelope().getChannelId())
&& Objects.equals(session.getActorType(), actor.getType())
&& Objects.equals(session.getActorId(), actor.getId())
&& Objects.equals(session.getOriginEntryType(), command.entryType().name())
&& Objects.equals(session.getConversationId(), command.userInput().getConversationId());
if (!matches) {
throw new IllegalArgumentException("Agent run session identity does not match the command");
}
}
}
@@ -0,0 +1,89 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.List;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.contract.AgentRunRequestSnapshot;
import org.apache.hertzbeat.ai.gateway.runtime.AgentApprovalHandling;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.util.JsonUtil;
/**
* Stable hash of fields that can change one runtime invocation's behavior.
*/
public final class AgentRunRequestFingerprint {
public static final String VERSION = "1";
private AgentRunRequestFingerprint() {
}
static String from(InvokeCommand command, AgentApprovalHandling approvalHandling) {
return from(snapshot(command, approvalHandling));
}
public static String from(AgentRunRequestSnapshot request) {
FingerprintMaterial material = new FingerprintMaterial(
request.entryType(), request.target(), request.alertIncident(), request.message(),
List.copyOf(request.attachments()), GatewayText.normalize(request.preferredLanguage()),
request.approvalHandling(), request.replyMode());
String canonical = JsonUtil.toJson(material);
if (!org.springframework.util.StringUtils.hasText(canonical)) {
throw new IllegalStateException("Agent run request fingerprint material cannot be serialized");
}
try {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(canonical.getBytes(StandardCharsets.UTF_8));
return "sha256:" + HexFormat.of().formatHex(digest);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required for Agent run request identity", exception);
}
}
static AgentRunRequestSnapshot snapshot(InvokeCommand command, AgentApprovalHandling approvalHandling) {
return AgentRunRequestSnapshot.builder()
.version(AgentRunRequestSnapshot.VERSION)
.conversationId(command.userInput().getConversationId())
.messageId(command.userInput().getMessageId())
.entryType(command.entryType().name())
.target(command.userInput().getTarget())
.alertIncident(command.userInput().getAlertIncident())
.message(command.userInput().getMessage().getText())
.attachments(command.userInput().getMessage().getAttachments())
.preferredLanguage(GatewayText.normalize(command.envelope().getPreferredLanguage()))
.approvalHandling(approvalHandling.name())
.replyMode(command.replyMode().name())
.build();
}
private record FingerprintMaterial(
String entryType,
Object target,
Object alertIncident,
String message,
List<String> attachments,
String preferredLanguage,
String approvalHandling,
String replyMode) {
}
}
@@ -0,0 +1,159 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.alert.service.AlertService;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves and rechecks the persisted authority for an exact workspace-owned SingleAlert. */
@Service
public class AgentSingleAlertTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "single-alert.";
public static final String AUTHORITY_VERSION_PREFIX = "single-alert-authority.";
public static final String TARGET_VERSION = "single-alert.v1";
public static final String AUTHORITY_VERSION = "single-alert-authority.v1";
public static final String ALERT_TYPE = "single";
private final AlertService alertService;
public AgentSingleAlertTargetAuthorityService(AlertService alertService) {
this.alertService = alertService;
}
public AgentTargetRef canonicalize(String workspaceId, long alertId) {
if (!StringUtils.hasText(workspaceId) || alertId <= 0) {
throw unavailable();
}
SingleAlert alert;
try {
alert = alertService.findSingleAlert(workspaceId, alertId)
.filter(candidate -> Objects.equals(workspaceId, candidate.getWorkspaceId()))
.filter(candidate -> Objects.equals(alertId, candidate.getId()))
.orElseThrow(this::unavailable);
} catch (UnavailableException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.alertId(alertId)
.alertType(ALERT_TYPE)
.authority(AgentTargetAuthority.builder()
.bindingId(alertId)
.version(AUTHORITY_VERSION)
.hash(authorityHash(workspaceId, alert))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return matches(target, canonicalize(workspaceId, target.getAlertId()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& TARGET_VERSION.equals(target.getVersion())
&& target.getAlertId() != null && target.getAlertId() > 0
&& ALERT_TYPE.equals(target.getAlertType())
&& authority != null
&& Objects.equals(target.getAlertId(), authority.getBindingId())
&& AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getEntityId() == null
&& target.getCollector() == null && target.getSignal() == null
&& target.getTopology() == null && target.getTrace() == null && target.getLog() == null
&& target.getService() == null;
}
public boolean matches(AgentTargetRef expected, AgentTargetRef actual) {
return isCanonicalTarget(expected) && Objects.equals(expected, actual);
}
private String authorityHash(String workspaceId, SingleAlert alert) {
StringBuilder material = new StringBuilder("single-alert-authority.v1;");
append(material, "workspace", workspaceId);
append(material, "id", alert.getId());
append(material, "fingerprint", alert.getFingerprint());
append(material, "status", alert.getStatus());
append(material, "content", alert.getContent());
append(material, "triggerTimes", alert.getTriggerTimes());
append(material, "startAt", alert.getStartAt());
append(material, "activeAt", alert.getActiveAt());
append(material, "endAt", alert.getEndAt());
appendMap(material, "labels", alert.getLabels());
appendMap(material, "annotations", alert.getAnnotations());
try {
return "sha256:" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(material.toString().getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required for alert target authority", exception);
}
}
private void appendMap(StringBuilder material, String field, Map<String, String> values) {
append(material, field + "Size", values == null ? null : values.size());
if (values != null) {
new TreeMap<>(values).forEach((key, value) -> {
append(material, field + "Key", key);
append(material, field + "Value", value);
});
}
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : String.valueOf(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Single alert target is unavailable");
}
}
}
@@ -0,0 +1,506 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.contract.AgentServiceRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentSignalRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.manager.service.entity.EntityMonitorMetricTargetCanonicalizer;
import org.apache.hertzbeat.manager.service.metric.MonitorMetricQueryContract;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
/** Adapts untrusted Gateway monitor-metric intent to the manager-authoritative target contract. */
@Service
public class AgentTargetCanonicalizationService {
private final EntityMonitorMetricTargetCanonicalizer canonicalizer;
private final AgentSingleAlertTargetAuthorityService alertAuthorityService;
private final AgentEntityTargetAuthorityService entityAuthorityService;
private final AgentTopologyTargetAuthorityService topologyAuthorityService;
private final AgentTraceTargetCanonicalizationAdapter traceAdapter;
private final AgentLogTargetCanonicalizationAdapter logAdapter;
public AgentTargetCanonicalizationService(EntityMonitorMetricTargetCanonicalizer canonicalizer,
AgentSingleAlertTargetAuthorityService alertAuthorityService,
AgentEntityTargetAuthorityService entityAuthorityService,
AgentTopologyTargetAuthorityService topologyAuthorityService) {
this(canonicalizer, alertAuthorityService, entityAuthorityService, topologyAuthorityService, null, null);
}
public AgentTargetCanonicalizationService(EntityMonitorMetricTargetCanonicalizer canonicalizer,
AgentSingleAlertTargetAuthorityService alertAuthorityService,
AgentEntityTargetAuthorityService entityAuthorityService,
AgentTopologyTargetAuthorityService topologyAuthorityService,
AgentTraceTargetCanonicalizationAdapter traceAdapter) {
this(canonicalizer, alertAuthorityService, entityAuthorityService, topologyAuthorityService,
traceAdapter, null);
}
@Autowired
public AgentTargetCanonicalizationService(EntityMonitorMetricTargetCanonicalizer canonicalizer,
AgentSingleAlertTargetAuthorityService alertAuthorityService,
AgentEntityTargetAuthorityService entityAuthorityService,
AgentTopologyTargetAuthorityService topologyAuthorityService,
AgentTraceTargetCanonicalizationAdapter traceAdapter,
AgentLogTargetCanonicalizationAdapter logAdapter) {
this.canonicalizer = canonicalizer;
this.alertAuthorityService = alertAuthorityService;
this.entityAuthorityService = entityAuthorityService;
this.topologyAuthorityService = topologyAuthorityService;
this.traceAdapter = traceAdapter;
this.logAdapter = logAdapter;
}
public boolean requiresCanonicalization(InvokeCommand command) {
AgentTargetRef target = command.userInput().getTarget();
AgentSignalRef signal = target == null ? null : target.getSignal();
return command.entryType() == AgentRuntimeEntryType.USER_INPUT
&& target != null && (hasCanonicalMarker(target) || isAlertIntent(target) || isTopologyIntent(target)
|| isEntityIntent(target) || traceAdapter != null && traceAdapter.isIntent(target)
|| logAdapter != null && logAdapter.isIntent(target)
|| target.getMonitorId() != null && signal != null && "metrics".equals(signal.getType()));
}
public InvokeCommand canonicalize(InvokeCommand sourceCommand) {
if (isAlertIntent(sourceCommand.userInput().getTarget())) {
return canonicalizeAlert(sourceCommand);
}
if (isTopologyIntent(sourceCommand.userInput().getTarget())) {
return canonicalizeTopology(sourceCommand);
}
if (traceAdapter != null && traceAdapter.isIntent(sourceCommand.userInput().getTarget())) {
return traceAdapter.canonicalize(sourceCommand);
}
if (logAdapter != null && logAdapter.isIntent(sourceCommand.userInput().getTarget())) {
return logAdapter.canonicalize(sourceCommand);
}
if (isEntityIntent(sourceCommand.userInput().getTarget())) {
return canonicalizeEntity(sourceCommand);
}
AgentTargetRef source = requireSourceIntent(sourceCommand.userInput().getTarget());
AgentSignalRef signal = source.getSignal();
EntityMonitorMetricTargetCanonicalizer.CanonicalTarget canonical;
try {
canonical = canonicalizer.canonicalize(sourceCommand.envelope().getWorkspaceId(),
new EntityMonitorMetricTargetCanonicalizer.SourceIntent(source.getMonitorId(), signal.getType(),
signal.getQuery(), signal.getStart(), signal.getEnd(), signal.getTimezone()));
} catch (EntityMonitorMetricTargetCanonicalizer.CanonicalizationException failure) {
throw new TargetCanonicalizationException(
failure.kind() == EntityMonitorMetricTargetCanonicalizer.FailureKind.MISMATCH
? FailureKind.MISMATCH : FailureKind.UNAVAILABLE);
} catch (RuntimeException ignored) {
throw unavailable();
}
if (canonical == null || canonical.service() == null || canonical.signal() == null
|| canonical.authority() == null) {
throw unavailable();
}
AgentTargetRef target = AgentTargetRef.builder()
.version(canonical.version())
.entityId(canonical.entityId())
.monitorId(canonical.monitorId())
.service(AgentServiceRef.builder()
.name(canonical.service().name())
.namespace(canonical.service().namespace())
.environment(canonical.service().environment())
.build())
.signal(AgentSignalRef.builder()
.type(canonical.signal().type())
.query(canonical.signal().query())
.start(canonical.signal().start())
.end(canonical.signal().end())
.timezone(canonical.signal().timezone())
.build())
.authority(AgentTargetAuthority.builder()
.bindingId(canonical.authority().bindingId())
.version(canonical.authority().version())
.hash(canonical.authority().hash())
.build())
.build();
requireSafeCanonicalTarget(target);
return withTarget(sourceCommand, target);
}
public InvokeCommand replayCommand(InvokeCommand sourceCommand, AgentTargetRef persistedTarget) {
if (isAlertIntent(sourceCommand.userInput().getTarget())) {
AgentTargetRef source = requireAlertSourceIntent(sourceCommand.userInput().getTarget());
if (!isCanonicalAlertTarget(persistedTarget)
|| !Objects.equals(alertSourceIntent(source), alertSourceIntent(persistedTarget))) {
throw mismatch();
}
return withTarget(sourceCommand, persistedTarget);
}
if (isTopologyIntent(sourceCommand.userInput().getTarget())) {
AgentTargetRef source = requireTopologySourceIntent(sourceCommand.userInput().getTarget());
if (!isCanonicalTopologyTarget(persistedTarget)
|| !Objects.equals(normalizedTopologySourceIntent(source), topologySourceIntent(persistedTarget))) {
throw mismatch();
}
return withTarget(sourceCommand, persistedTarget);
}
if (traceAdapter != null && traceAdapter.isIntent(sourceCommand.userInput().getTarget())) {
return traceAdapter.replayCommand(sourceCommand, persistedTarget);
}
if (logAdapter != null && logAdapter.isIntent(sourceCommand.userInput().getTarget())) {
return logAdapter.replayCommand(sourceCommand, persistedTarget);
}
if (isEntityIntent(sourceCommand.userInput().getTarget())) {
AgentTargetRef source = requireEntitySourceIntent(sourceCommand.userInput().getTarget());
if (!isCanonicalEntityTarget(persistedTarget)
|| !Objects.equals(entitySourceIntent(source), entitySourceIntent(persistedTarget))) {
throw mismatch();
}
return withTarget(sourceCommand, persistedTarget);
}
AgentTargetRef source = requireSourceIntent(sourceCommand.userInput().getTarget());
if (!isCanonicalTarget(persistedTarget)
|| !Objects.equals(sourceIntent(source), sourceIntent(persistedTarget))) {
throw mismatch();
}
return withTarget(sourceCommand, persistedTarget);
}
public static AgentTargetRef retrySourceIntent(AgentTargetRef target) {
if (isCanonicalAlertTarget(target)) {
return alertSourceIntent(target);
}
if (isCanonicalEntityTarget(target)) {
return entitySourceIntent(target);
}
if (isCanonicalTopologyTarget(target)) {
return topologySourceIntent(target);
}
if (AgentTraceTargetAuthorityService.TARGET_VERSION.equals(target == null ? null : target.getVersion())) {
return AgentTraceTargetCanonicalizationAdapter.sourceIntent(target);
}
if (AgentLogTargetAuthorityService.TARGET_VERSION.equals(target == null ? null : target.getVersion())) {
return AgentLogTargetCanonicalizationAdapter.sourceIntent(target);
}
return isCanonicalTarget(target) ? sourceIntent(target) : target;
}
public static boolean isCanonicalTarget(AgentTargetRef target) {
return target != null
&& EntityMonitorMetricTargetCanonicalizer.TARGET_VERSION.equals(target.getVersion())
&& target.getEntityId() != null && target.getMonitorId() != null
&& target.getService() != null && target.getAuthority() != null
&& target.getSignal() != null && "metrics".equals(target.getSignal().getType())
&& target.getAlertId() == null && target.getAlertType() == null
&& target.getCollector() == null && target.getTopology() == null && target.getTrace() == null
&& target.getLog() == null;
}
public static boolean isCanonicalAlertTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& AgentSingleAlertTargetAuthorityService.TARGET_VERSION.equals(target.getVersion())
&& target.getAlertId() != null && target.getAlertId() > 0
&& AgentSingleAlertTargetAuthorityService.ALERT_TYPE.equals(target.getAlertType())
&& authority != null
&& Objects.equals(target.getAlertId(), authority.getBindingId())
&& AgentSingleAlertTargetAuthorityService.AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getEntityId() == null
&& target.getCollector() == null && target.getSignal() == null
&& target.getTopology() == null && target.getTrace() == null && target.getLog() == null
&& target.getService() == null;
}
public static boolean isCanonicalEntityTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& AgentEntityTargetAuthorityService.TARGET_VERSION.equals(target.getVersion())
&& target.getEntityId() != null && target.getEntityId() > 0
&& authority != null
&& Objects.equals(target.getEntityId(), authority.getBindingId())
&& AgentEntityTargetAuthorityService.AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getAlertId() == null
&& target.getAlertType() == null && target.getCollector() == null
&& target.getSignal() == null && target.getTopology() == null && target.getTrace() == null
&& target.getLog() == null
&& target.getService() == null;
}
public static boolean isCanonicalTopologyTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& AgentTopologyTargetAuthorityService.TARGET_VERSION.equals(target.getVersion())
&& target.getEntityId() != null && target.getEntityId() > 0
&& target.getTopology() != null
&& Objects.equals(target.getEntityId(), target.getTopology().getRootEntityId())
&& authority != null
&& Objects.equals(target.getEntityId(), authority.getBindingId())
&& AgentTopologyTargetAuthorityService.AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getAlertId() == null && target.getAlertType() == null
&& target.getCollector() == null && target.getSignal() == null && target.getTrace() == null
&& target.getLog() == null
&& target.getService() == null;
}
private AgentTargetRef requireSourceIntent(AgentTargetRef target) {
if (target == null || target.getMonitorId() == null || target.getSignal() == null
|| target.getVersion() != null || target.getEntityId() != null || target.getService() != null
|| target.getAuthority() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getCollector() != null
|| target.getTopology() != null || target.getTrace() != null || target.getLog() != null
|| target.getSignal().getTimeRange() != null
|| !MonitorMetricQueryContract.isExactWindowAllowed(
target.getSignal().getStart(), target.getSignal().getEnd())) {
throw mismatch();
}
return target;
}
private static AgentTargetRef sourceIntent(AgentTargetRef target) {
AgentSignalRef signal = target.getSignal();
return AgentTargetRef.builder()
.monitorId(target.getMonitorId())
.signal(AgentSignalRef.builder()
.type(signal.getType())
.query(signal.getQuery())
.start(signal.getStart())
.end(signal.getEnd())
.timezone(signal.getTimezone())
.build())
.build();
}
private InvokeCommand canonicalizeAlert(InvokeCommand command) {
AgentTargetRef source = requireAlertSourceIntent(command.userInput().getTarget());
if (alertAuthorityService == null) {
throw unavailable();
}
try {
AgentTargetRef target = alertAuthorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getAlertId());
requireSafeCanonicalTarget(target);
return withTarget(command, target);
} catch (AgentSingleAlertTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (RuntimeException ignored) {
throw unavailable();
}
}
private AgentTargetRef requireAlertSourceIntent(AgentTargetRef target) {
if (target == null || target.getAlertId() == null || target.getAlertId() <= 0
|| !AgentSingleAlertTargetAuthorityService.ALERT_TYPE.equals(target.getAlertType())
|| target.getVersion() != null || target.getAuthority() != null
|| target.getMonitorId() != null || target.getEntityId() != null
|| target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getTrace() != null || target.getLog() != null
|| target.getService() != null) {
throw unavailable();
}
return target;
}
private static AgentTargetRef alertSourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder()
.alertId(target.getAlertId())
.alertType(AgentSingleAlertTargetAuthorityService.ALERT_TYPE)
.build();
}
private InvokeCommand canonicalizeEntity(InvokeCommand command) {
AgentTargetRef source = requireEntitySourceIntent(command.userInput().getTarget());
if (entityAuthorityService == null) {
throw unavailable();
}
try {
AgentTargetRef target = entityAuthorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getEntityId());
requireSafeCanonicalTarget(target);
return withTarget(command, target);
} catch (AgentEntityTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (RuntimeException ignored) {
throw unavailable();
}
}
private AgentTargetRef requireEntitySourceIntent(AgentTargetRef target) {
if (target == null || target.getEntityId() == null || target.getEntityId() <= 0
|| target.getVersion() != null || target.getAuthority() != null
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getTrace() != null || target.getLog() != null
|| target.getService() != null) {
throw unavailable();
}
return target;
}
private static AgentTargetRef entitySourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder().entityId(target.getEntityId()).build();
}
private InvokeCommand canonicalizeTopology(InvokeCommand command) {
AgentTargetRef source = requireTopologySourceIntent(command.userInput().getTarget());
if (topologyAuthorityService == null) {
throw unavailable();
}
try {
AgentTargetRef target = topologyAuthorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getTopology());
requireSafeCanonicalTarget(target);
return withTarget(command, target);
} catch (AgentTopologyTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (RuntimeException ignored) {
throw unavailable();
}
}
private AgentTargetRef requireTopologySourceIntent(AgentTargetRef target) {
if (target == null || target.getTopology() == null
|| target.getTopology().getRootEntityId() == null || target.getTopology().getRootEntityId() <= 0
|| target.getTopology().getDepth() == null || target.getTopology().getDepth() < 1
|| target.getTopology().getDepth() > 2
|| target.getTopology().getNodeId() != null && target.getTopology().getEdgeId() != null
|| target.getVersion() != null || target.getAuthority() != null || target.getEntityId() != null
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getCollector() != null || target.getSignal() != null || target.getTrace() != null
|| target.getLog() != null
|| target.getService() != null) {
throw unavailable();
}
return target;
}
private static AgentTargetRef topologySourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder().topology(target.getTopology()).build();
}
private AgentTargetRef normalizedTopologySourceIntent(AgentTargetRef target) {
if (topologyAuthorityService == null) {
throw mismatch();
}
try {
return AgentTargetRef.builder()
.topology(topologyAuthorityService.normalizeSource(target.getTopology()))
.build();
} catch (RuntimeException ignored) {
throw mismatch();
}
}
private boolean isAlertIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getAlertId() != null || target.getAlertType() != null
|| hasSingleAlertVersion(target.getVersion(), AgentSingleAlertTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasSingleAlertVersion(
authority.getVersion(), AgentSingleAlertTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
private boolean isEntityIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getTopology() == null && target.getTrace() == null && target.getLog() == null
&& target.getEntityId() != null && (target.getMonitorId() == null || target.getSignal() == null)
|| hasEntityVersion(target.getVersion(), AgentEntityTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasEntityVersion(
authority.getVersion(), AgentEntityTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
private boolean isTopologyIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getTopology() != null
|| hasTopologyVersion(target.getVersion(), AgentTopologyTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasTopologyVersion(
authority.getVersion(), AgentTopologyTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
private boolean hasCanonicalMarker(AgentTargetRef target) {
return target.getVersion() != null || target.getAuthority() != null;
}
private boolean hasSingleAlertVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private boolean hasEntityVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private boolean hasTopologyVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private InvokeCommand withTarget(InvokeCommand command, AgentTargetRef target) {
UserInput userInput = command.userInput().toBuilder().target(target).build();
return new InvokeCommand(command.envelope(), command.replyMode(), command.commandId(), userInput,
command.entryType());
}
private void requireSafeCanonicalTarget(AgentTargetRef target) {
String json = JsonUtil.toJson(target);
if (!StringUtils.hasText(json) || !Objects.equals(json, GatewayText.redactSecrets(json))) {
throw unavailable();
}
}
private TargetCanonicalizationException mismatch() {
return new TargetCanonicalizationException(FailureKind.MISMATCH);
}
private TargetCanonicalizationException unavailable() {
return new TargetCanonicalizationException(FailureKind.UNAVAILABLE);
}
/** Stable cause-free failure categories for channel presentation. */
public enum FailureKind {
MISMATCH,
UNAVAILABLE
}
/** Cause-free exception used at the channel boundary. */
public static final class TargetCanonicalizationException extends IllegalArgumentException {
private final FailureKind kind;
public TargetCanonicalizationException(FailureKind kind) {
super(kind == FailureKind.MISMATCH ? "Investigation target does not match"
: "Investigation target is unavailable");
this.kind = kind;
}
public FailureKind kind() {
return kind;
}
}
}
@@ -0,0 +1,214 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.HexFormat;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentTopologyRef;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves a focused Topology scope through its workspace-owned root Entity. */
@Service
public class AgentTopologyTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "topology.";
public static final String AUTHORITY_VERSION_PREFIX = "topology-authority.";
public static final String TARGET_VERSION = "topology.v1";
public static final String AUTHORITY_VERSION = "topology-authority.v1";
private static final long MAX_RANGE_MILLIS = Duration.ofDays(7).toMillis();
private static final Set<String> SOURCE_KINDS = Set.of(
"all", "alert-impact", "entity-relation", "monitor-bind", "monitor-ownership",
"otlp-trace-call", "k8s-workload", "cmdb-manual-label", "database-middleware-connection",
"template-dependency");
private final AgentEntityTargetAuthorityService entityAuthorityService;
public AgentTopologyTargetAuthorityService(AgentEntityTargetAuthorityService entityAuthorityService) {
this.entityAuthorityService = entityAuthorityService;
}
public AgentTargetRef canonicalize(String workspaceId, AgentTopologyRef source) {
AgentTopologyRef topology = normalizeSource(source);
AgentTargetRef entityTarget;
try {
entityTarget = entityAuthorityService.canonicalize(workspaceId, topology.getRootEntityId());
} catch (RuntimeException ignored) {
throw unavailable();
}
if (!entityAuthorityService.isCanonicalTarget(entityTarget)
|| !Objects.equals(topology.getRootEntityId(), entityTarget.getEntityId())) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.entityId(topology.getRootEntityId())
.topology(topology)
.authority(AgentTargetAuthority.builder()
.bindingId(topology.getRootEntityId())
.version(AUTHORITY_VERSION)
.hash(authorityHash(entityTarget.getAuthority().getHash(), topology))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return Objects.equals(target, canonicalize(workspaceId, target.getTopology()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
if (target == null || !TARGET_VERSION.equals(target.getVersion())
|| target.getEntityId() == null || target.getEntityId() <= 0
|| target.getTopology() == null || authority == null
|| !Objects.equals(target.getEntityId(), target.getTopology().getRootEntityId())
|| !Objects.equals(target.getEntityId(), authority.getBindingId())
|| !AUTHORITY_VERSION.equals(authority.getVersion())
|| authority.getHash() == null || !authority.getHash().matches("sha256:[0-9a-f]{64}")
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getCollector() != null || target.getSignal() != null || target.getTrace() != null
|| target.getLog() != null
|| target.getService() != null) {
return false;
}
try {
return Objects.equals(target.getTopology(), normalizeSource(target.getTopology()));
} catch (UnavailableException ignored) {
return false;
}
}
public AgentTopologyRef normalizeSource(AgentTopologyRef source) {
if (source == null || source.getRootEntityId() == null || source.getRootEntityId() <= 0
|| source.getDepth() == null || source.getDepth() < 1 || source.getDepth() > 2
|| source.getNodeId() != null && source.getEdgeId() != null) {
throw unavailable();
}
String nodeId = text(source.getNodeId(), 512);
String edgeId = text(source.getEdgeId(), 512);
String environment = text(source.getEnvironment(), 128);
String relationType = text(source.getRelationType(), 128);
String sourceKind = source.getSourceKind() == null
? "entity-relation" : text(source.getSourceKind(), 64);
if (sourceKind == null || !SOURCE_KINDS.contains(sourceKind.toLowerCase(Locale.ROOT))) {
throw unavailable();
}
sourceKind = sourceKind.toLowerCase(Locale.ROOT);
validateRange(source.getStart(), source.getEnd());
int pageIndex = source.getPageIndex() == null ? 0 : source.getPageIndex();
int pageSize = source.getPageSize() == null ? 50 : source.getPageSize();
if (pageIndex < 0 || pageIndex > 10_000 || pageSize < 1 || pageSize > 100) {
throw unavailable();
}
return AgentTopologyRef.builder()
.rootEntityId(source.getRootEntityId())
.nodeId(nodeId)
.edgeId(edgeId)
.depth(source.getDepth())
.environment(environment)
.sourceKind(sourceKind)
.start(source.getStart())
.end(source.getEnd())
.relationType(relationType)
.hideInternal(Boolean.TRUE.equals(source.getHideInternal()))
.pageIndex(pageIndex)
.pageSize(pageSize)
.build();
}
private void validateRange(Long start, Long end) {
if (start == null && end == null) {
return;
}
if (start == null || end == null || start <= 0 || end <= start || end - start > MAX_RANGE_MILLIS) {
throw unavailable();
}
}
private String text(String value, int maximumLength) {
if (value == null) {
return null;
}
String normalized = value.trim();
if (!StringUtils.hasText(normalized) || normalized.length() > maximumLength
|| normalized.codePoints().anyMatch(code -> code < 32 || code == 127)) {
throw unavailable();
}
return normalized;
}
private String authorityHash(String entityAuthorityHash, AgentTopologyRef topology) {
StringBuilder material = new StringBuilder("topology-authority.v1;");
append(material, "entityAuthority", entityAuthorityHash);
append(material, "rootEntityId", topology.getRootEntityId());
append(material, "nodeId", topology.getNodeId());
append(material, "edgeId", topology.getEdgeId());
append(material, "depth", topology.getDepth());
append(material, "environment", topology.getEnvironment());
append(material, "sourceKind", topology.getSourceKind());
append(material, "start", topology.getStart());
append(material, "end", topology.getEnd());
append(material, "relationType", topology.getRelationType());
append(material, "hideInternal", topology.getHideInternal());
append(material, "pageIndex", topology.getPageIndex());
append(material, "pageSize", topology.getPageSize());
try {
return "sha256:" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(material.toString().getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required for topology target authority", exception);
}
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : String.valueOf(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Topology target is unavailable");
}
}
}
@@ -0,0 +1,213 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentTraceRef;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.observability.dto.trace.TraceDetailDto;
import org.apache.hertzbeat.common.observability.dto.trace.TraceSpanNodeDto;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.observability.traces.service.EntityTraceQueryService;
import org.apache.hertzbeat.observability.traces.service.EntityTraceQueryService.TraceDetailQuery;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves one exact Trace Explore detail scope through the trusted-workspace query boundary. */
@Service
public class AgentTraceTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "trace-detail.";
public static final String AUTHORITY_VERSION_PREFIX = "trace-detail-authority.";
public static final String TARGET_VERSION = "trace-detail.v1";
public static final String AUTHORITY_VERSION = "trace-detail-authority.v1";
private static final long MAX_RANGE_MILLIS = Duration.ofDays(7).toMillis();
private static final Pattern SAFE_ID = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._:-]{0,127}");
private final EntityTraceQueryService traceQueryService;
public AgentTraceTargetAuthorityService(EntityTraceQueryService traceQueryService) {
this.traceQueryService = traceQueryService;
}
public AgentTargetRef canonicalize(String workspaceId, AgentTraceRef source) {
AgentTraceRef trace = normalizeSource(source);
TraceDetailDto detail;
try {
detail = traceQueryService.getTraceDetail(workspaceId, detailQuery(trace));
} catch (RuntimeException ignored) {
throw unavailable();
}
if (detail == null || !Objects.equals(trace.getTraceId(), detail.getTraceId())) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.trace(trace)
.authority(AgentTargetAuthority.builder()
.version(AUTHORITY_VERSION)
.hash("sha256:" + GatewayText.sha256(authorityMaterial(workspaceId, trace, detail)))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return Objects.equals(target, canonicalize(workspaceId, target.getTrace()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
if (target == null || !TARGET_VERSION.equals(target.getVersion()) || target.getTrace() == null
|| authority == null || authority.getBindingId() != null
|| !AUTHORITY_VERSION.equals(authority.getVersion())
|| authority.getHash() == null || !authority.getHash().matches("sha256:[0-9a-f]{64}")
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getEntityId() != null || target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getLog() != null || target.getService() != null) {
return false;
}
try {
return Objects.equals(target.getTrace(), normalizeSource(target.getTrace()));
} catch (UnavailableException ignored) {
return false;
}
}
public AgentTraceRef normalizeSource(AgentTraceRef source) {
if (source == null || !safeId(source.getTraceId()) || source.getSpanId() != null && !safeId(source.getSpanId())
|| source.getStart() == null || source.getEnd() == null || source.getStart() <= 0
|| source.getEnd() <= source.getStart() || source.getEnd() - source.getStart() > MAX_RANGE_MILLIS
|| source.getMinDurationMs() != null && source.getMinDurationMs() < 0
|| source.getMaxDurationMs() != null && source.getMaxDurationMs() < 0
|| source.getMinDurationMs() != null && source.getMaxDurationMs() != null
&& source.getMinDurationMs() > source.getMaxDurationMs()) {
throw unavailable();
}
return AgentTraceRef.builder()
.traceId(source.getTraceId())
.spanId(source.getSpanId())
.start(source.getStart())
.end(source.getEnd())
.serviceName(text(source.getServiceName(), 512))
.serviceNamespace(text(source.getServiceNamespace(), 512))
.environment(text(source.getEnvironment(), 512))
.resourceFilter(text(source.getResourceFilter(), 2048))
.attributeFilter(text(source.getAttributeFilter(), 2048))
.minDurationMs(source.getMinDurationMs())
.maxDurationMs(source.getMaxDurationMs())
.build();
}
private TraceDetailQuery detailQuery(AgentTraceRef trace) {
return new TraceDetailQuery(null, trace.getTraceId(), trace.getSpanId(), trace.getStart(), trace.getEnd(),
trace.getServiceName(), trace.getServiceNamespace(), trace.getEnvironment(), trace.getResourceFilter(),
trace.getAttributeFilter(), trace.getMinDurationMs(), trace.getMaxDurationMs());
}
private String authorityMaterial(String workspaceId, AgentTraceRef trace, TraceDetailDto detail) {
StringBuilder material = new StringBuilder("trace-detail-authority.v1;");
append(material, "workspaceId", workspaceId);
append(material, "scope", JsonUtil.toJson(trace));
append(material, "traceId", detail.getTraceId());
append(material, "rootSpanId", detail.getRootSpanId());
append(material, "serviceName", detail.getServiceName());
append(material, "serviceNamespace", detail.getServiceNamespace());
append(material, "rootSpanName", detail.getRootSpanName());
append(material, "durationNanos", detail.getDurationNanos());
append(material, "status", detail.getStatus());
append(material, "startTime", detail.getStartTime());
append(material, "errorSpanCount", detail.getErrorSpanCount());
append(material, "resourceAttributes", sorted(detail.getResourceAttributes()));
if (detail.getSpans() != null) {
for (TraceSpanNodeDto span : detail.getSpans()) {
appendSpan(material, span);
}
}
return material.toString();
}
private void appendSpan(StringBuilder material, TraceSpanNodeDto span) {
append(material, "span.traceId", span == null ? null : span.getTraceId());
append(material, "span.spanId", span == null ? null : span.getSpanId());
append(material, "span.parentSpanId", span == null ? null : span.getParentSpanId());
append(material, "span.name", span == null ? null : span.getSpanName());
append(material, "span.service", span == null ? null : span.getServiceName());
append(material, "span.status", span == null ? null : span.getStatus());
append(material, "span.kind", span == null ? null : span.getSpanKind());
append(material, "span.duration", span == null ? null : span.getDurationNanos());
append(material, "span.start", span == null ? null : span.getStartTime());
append(material, "span.resource", span == null ? null : sorted(span.getResourceAttributes()));
append(material, "span.attributes", span == null ? null : sorted(span.getSpanAttributes()));
}
private Map<String, String> sorted(Map<String, String> values) {
return values == null ? Map.of() : new TreeMap<>(values);
}
private String text(String value, int maximumLength) {
if (value == null) {
return null;
}
String normalized = value.trim();
if (!StringUtils.hasText(normalized) || normalized.length() > maximumLength
|| normalized.codePoints().anyMatch(code -> code < 32 || code == 127)
|| !Objects.equals(normalized, GatewayText.redactSecrets(normalized))) {
throw unavailable();
}
return normalized;
}
private boolean safeId(String value) {
return value != null && SAFE_ID.matcher(value).matches();
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : String.valueOf(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Trace target is unavailable");
}
}
}
@@ -0,0 +1,120 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.FailureKind;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.TargetCanonicalizationException;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Isolates strict source/canonical adaptation for an exact Trace Explore detail target. */
@Service
public class AgentTraceTargetCanonicalizationAdapter {
private final AgentTraceTargetAuthorityService authorityService;
public AgentTraceTargetCanonicalizationAdapter(AgentTraceTargetAuthorityService authorityService) {
this.authorityService = authorityService;
}
public boolean isIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getTrace() != null
|| hasVersion(target.getVersion(), AgentTraceTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasVersion(
authority.getVersion(), AgentTraceTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
public GatewayCommand.InvokeCommand canonicalize(GatewayCommand.InvokeCommand command) {
AgentTargetRef source = requireSourceIntent(command.userInput().getTarget());
try {
AgentTargetRef target = authorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getTrace());
if (!authorityService.isCanonicalTarget(target)
|| !Objects.equals(authorityService.normalizeSource(source.getTrace()), target.getTrace())) {
throw unavailable();
}
return withTarget(command, target);
} catch (AgentTraceTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (TargetCanonicalizationException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw unavailable();
}
}
public GatewayCommand.InvokeCommand replayCommand(
GatewayCommand.InvokeCommand command, AgentTargetRef persistedTarget) {
AgentTargetRef source = requireSourceIntent(command.userInput().getTarget());
try {
if (!authorityService.isCanonicalTarget(persistedTarget)
|| !Objects.equals(authorityService.normalizeSource(source.getTrace()), persistedTarget.getTrace())) {
throw mismatch();
}
return withTarget(command, persistedTarget);
} catch (TargetCanonicalizationException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw mismatch();
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
return authorityService.isCanonicalTarget(target);
}
public static AgentTargetRef sourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder().trace(target.getTrace()).build();
}
private AgentTargetRef requireSourceIntent(AgentTargetRef target) {
if (target == null || target.getTrace() == null || target.getVersion() != null || target.getAuthority() != null
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getEntityId() != null || target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getLog() != null || target.getService() != null) {
throw unavailable();
}
return target;
}
private GatewayCommand.InvokeCommand withTarget(GatewayCommand.InvokeCommand command, AgentTargetRef target) {
UserInput userInput = command.userInput().toBuilder().target(target).build();
return new GatewayCommand.InvokeCommand(command.envelope(), command.replyMode(), command.commandId(), userInput,
command.entryType());
}
private boolean hasVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private TargetCanonicalizationException mismatch() {
return new TargetCanonicalizationException(FailureKind.MISMATCH);
}
private TargetCanonicalizationException unavailable() {
return new TargetCanonicalizationException(FailureKind.UNAVAILABLE);
}
}
@@ -0,0 +1,116 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ApprovalDecisionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeApprovalRegistry;
import org.apache.hertzbeat.ai.gateway.tool.core.AgentToolCallLedgerService;
import org.apache.hertzbeat.common.entity.agent.AgentToolCall;
import org.springframework.stereotype.Service;
/**
* Approval decision commands for active runtime loops.
*/
@Service
public class ApprovalCommandService {
private static final String STATUS_COMPLETED = "completed";
private static final String STATUS_FAILED = "failed";
private static final Duration DEFAULT_CONSUMPTION_TIMEOUT = Duration.ofSeconds(5);
private final AgentToolCallLedgerService toolCallLedgerService;
private final AgentRuntimeApprovalRegistry approvalRegistry;
private final Duration consumptionTimeout;
@org.springframework.beans.factory.annotation.Autowired
public ApprovalCommandService(AgentToolCallLedgerService toolCallLedgerService,
AgentRuntimeApprovalRegistry approvalRegistry) {
this(toolCallLedgerService, approvalRegistry, DEFAULT_CONSUMPTION_TIMEOUT);
}
ApprovalCommandService(AgentToolCallLedgerService toolCallLedgerService,
AgentRuntimeApprovalRegistry approvalRegistry,
Duration consumptionTimeout) {
this.toolCallLedgerService = toolCallLedgerService;
this.approvalRegistry = approvalRegistry;
this.consumptionTimeout = consumptionTimeout;
}
public GatewaySingleResponse decide(ApprovalDecisionCommand command) {
toolCallLedgerService.requireApprovalOwner(command.approvalId(), command.envelope(),
command.originEntryType());
var reservation = approvalRegistry.reserve(command.approvalId());
if (reservation.isEmpty()) {
return response(command, null, List.of(errorEvent(command, null,
"Agent approval runtime loop is no longer active.")));
}
AgentToolCall approval;
try {
approval = toolCallLedgerService.decideApproval(command.approvalId(), command.envelope(),
command.originEntryType(), command.decision());
} catch (RuntimeException | Error failure) {
reservation.orElseThrow().release();
throw failure;
}
var delivery = reservation.orElseThrow().deliver(command.decision());
if (!delivery.accepted() || !delivery.awaitConsumption(consumptionTimeout)) {
AgentToolCall terminal = toolCallLedgerService.terminalizeUnconsumedApproval(
command.approvalId(), command.envelope(), command.originEntryType(), command.decision());
return response(command, terminal, List.of(errorEvent(command, terminal,
"Agent approval runtime loop is no longer active.")));
}
return response(command, approval, List.of());
}
private GatewaySingleResponse response(ApprovalDecisionCommand command, AgentToolCall approval,
List<GatewayEvent> events) {
boolean success = events.isEmpty();
Meta meta = Meta.builder()
.commandId(command.commandId())
.sessionUid(approval == null ? null : approval.getSessionUid())
.runUid(approval == null ? null : approval.getRunUid())
.terminal(true)
.message(success ? "approval accepted" : "approval failed")
.build();
return GatewaySingleResponse.builder()
.meta(meta)
.body(Map.of("status", success ? STATUS_COMPLETED : STATUS_FAILED))
.events(events)
.build();
}
private GatewayEvent errorEvent(ApprovalDecisionCommand command, AgentToolCall approval, String message) {
return GatewayEvent.builder()
.type(GatewayEventType.ERROR)
.eventId(command.commandId() + ":error")
.sessionUid(approval == null ? null : approval.getSessionUid())
.runUid(approval == null ? null : approval.getRunUid())
.payload(ErrorPayload.builder()
.errorMessage(message)
.build())
.timestamp(System.currentTimeMillis())
.build();
}
}
@@ -0,0 +1,419 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.Objects;
import lombok.Builder;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.identity.ActorSupport;
import org.apache.hertzbeat.ai.gateway.tool.core.AgentApprovalDecision;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.springframework.util.StringUtils;
/**
* Strongly typed Gateway command.
*/
public sealed interface GatewayCommand permits
GatewayCommand.InvokeCommand,
GatewayCommand.ApprovalDecisionCommand,
GatewayCommand.CancelRunCommand,
GatewayCommand.GetSessionCommand,
GatewayCommand.GetRunCommand,
GatewayCommand.GetLatestSessionRunCommand,
GatewayCommand.ListSessionsCommand,
GatewayCommand.GetSessionTranscriptCommand,
GatewayCommand.ListModelProviderOptionsCommand,
GatewayCommand.ListModelProviderConfigurationsCommand,
GatewayCommand.CreateModelProviderConfigurationCommand,
GatewayCommand.UpdateModelProviderConfigurationCommand,
GatewayCommand.DeleteModelProviderConfigurationCommand,
GatewayCommand.SwitchModelProviderCommand {
GatewayEnvelope envelope();
ReplyMode replyMode();
String commandId();
/**
* Normalized runtime invocation shared by interactive and system-triggered entries.
*/
@Builder
record InvokeCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
UserInput userInput,
AgentRuntimeEntryType entryType) implements GatewayCommand {
public InvokeCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
userInput = Objects.requireNonNull(userInput, "userInput is required");
entryType = Objects.requireNonNull(entryType, "entryType is required");
}
public InvokeCommand(GatewayEnvelope envelope, ReplyMode replyMode, String commandId, UserInput userInput) {
this(envelope, replyMode, commandId, userInput, AgentRuntimeEntryType.USER_INPUT);
}
}
/**
* Approval decision command.
*/
@Builder
record ApprovalDecisionCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String approvalId,
AgentApprovalDecision decision) implements GatewayCommand {
public ApprovalDecisionCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
decision = Objects.requireNonNull(decision, "approval decision is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(approvalId)) {
throw new IllegalArgumentException("commandId and approvalId are required");
}
}
}
/**
* Runtime cancellation command.
*/
@Builder
record CancelRunCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String runUid,
String reason) implements GatewayCommand {
public CancelRunCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(runUid)) {
throw new IllegalArgumentException("commandId and runUid are required");
}
}
}
/**
* Session query command.
*/
@Builder
record GetSessionCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String sessionUid) implements GatewayCommand {
public GetSessionCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
// The command must fully describe the session query scope.
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(sessionUid)) {
throw new IllegalArgumentException("commandId and sessionUid are required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Session query actor is required");
}
}
}
/** Owner-scoped durable run query. */
@Builder
record GetRunCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String runUid) implements GatewayCommand {
public GetRunCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(runUid)) {
throw new IllegalArgumentException("commandId and runUid are required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Run query actor is required");
}
}
}
/** Owner-scoped latest durable run for one session. */
@Builder
record GetLatestSessionRunCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String sessionUid) implements GatewayCommand {
public GetLatestSessionRunCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(sessionUid)) {
throw new IllegalArgumentException("commandId and sessionUid are required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Latest run query actor is required");
}
}
}
/**
* Current actor session list query.
*/
@Builder
record ListSessionsCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String title,
int pageIndex,
int pageSize) implements GatewayCommand {
public ListSessionsCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
// The command must fully describe the session query scope.
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Session query actor is required");
}
if (pageIndex < 0 || pageSize < 1) {
throw new IllegalArgumentException("Session page index and size are invalid");
}
pageSize = Math.min(pageSize, 200);
}
}
/**
* Current actor session transcript query.
*/
@Builder
record GetSessionTranscriptCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String sessionUid,
int pageIndex,
int pageSize) implements GatewayCommand {
public GetSessionTranscriptCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
// The command must fully describe the session query scope.
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(sessionUid)) {
throw new IllegalArgumentException("commandId and sessionUid are required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Session query actor is required");
}
if (pageIndex < 0 || pageSize < 1) {
throw new IllegalArgumentException("Session page index and size are invalid");
}
pageSize = Math.min(pageSize, 200);
}
}
/**
* Registered model provider option query.
*/
@Builder
record ListModelProviderOptionsCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId) implements GatewayCommand {
public ListModelProviderOptionsCommand {
// Channel commands require trusted invocation metadata and an explicit delivery mode.
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider option queries require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider option query actor is required");
}
}
}
/**
* Saved model provider configuration query.
*/
@Builder
record ListModelProviderConfigurationsCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId) implements GatewayCommand {
public ListModelProviderConfigurationsCommand {
// Channel commands require trusted invocation metadata and an explicit delivery mode.
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider configuration queries require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider configuration query actor is required");
}
}
}
/**
* Saved model provider configuration creation.
*/
@Builder
record CreateModelProviderConfigurationCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
ModelProviderConfig config) implements GatewayCommand {
public CreateModelProviderConfigurationCommand {
// Channel commands require trusted invocation metadata and an explicit delivery mode.
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider configuration creation requires final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider configuration creation actor is required");
}
// A create command owns the complete provider DTO so validation and normalization happen once downstream.
config = Objects.requireNonNull(config, "model provider config is required");
}
}
/**
* Saved model provider configuration update.
*/
@Builder
record UpdateModelProviderConfigurationCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
String providerUid,
ModelProviderConfig config) implements GatewayCommand {
public UpdateModelProviderConfigurationCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(providerUid)) {
throw new IllegalArgumentException("commandId and providerUid are required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider configuration updates require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider configuration update actor is required");
}
config = Objects.requireNonNull(config, "model provider config is required");
}
}
/**
* Saved model provider configuration deletion.
*/
@Builder
record DeleteModelProviderConfigurationCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
String providerUid) implements GatewayCommand {
public DeleteModelProviderConfigurationCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(providerUid)) {
throw new IllegalArgumentException("commandId and providerUid are required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider configuration deletions require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider configuration deletion actor is required");
}
}
}
/**
* Active model provider selection. A null UID selects the YAML default.
*/
@Builder
record SwitchModelProviderCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
String providerUid) implements GatewayCommand {
public SwitchModelProviderCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider switches require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider switch actor is required");
}
}
}
/**
* Response delivery mode requested by a channel.
*/
enum ReplyMode {
FINAL_ONLY,
STREAM
}
}
@@ -0,0 +1,90 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ApprovalDecisionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CancelRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CreateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.DeleteModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetLatestSessionRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionTranscriptCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderConfigurationsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderOptionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListSessionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.SwitchModelProviderCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.UpdateModelProviderConfigurationCommand;
import org.springframework.stereotype.Service;
/**
* Routes Gateway commands to domain services.
*/
@Service
public class GatewayCommandRouter {
private final AgentCommandService agentCommandService;
private final ApprovalCommandService approvalCommandService;
private final RunCommandService runCommandService;
private final GatewayQueryService queryService;
private final ModelProviderCommandService modelProviderCommandService;
public GatewayCommandRouter(AgentCommandService agentCommandService,
ApprovalCommandService approvalCommandService,
RunCommandService runCommandService,
GatewayQueryService queryService,
ModelProviderCommandService modelProviderCommandService) {
this.agentCommandService = Objects.requireNonNull(agentCommandService, "agentCommandService is required");
this.approvalCommandService = Objects.requireNonNull(approvalCommandService,
"approvalCommandService is required");
this.runCommandService = Objects.requireNonNull(runCommandService, "runCommandService is required");
this.queryService = Objects.requireNonNull(queryService, "queryService is required");
// Provider commands cannot be exposed unless their application service is wired.
this.modelProviderCommandService = Objects.requireNonNull(
modelProviderCommandService, "modelProviderCommandService is required");
}
public GatewayResponse handle(GatewayCommand command) {
Objects.requireNonNull(command, "command is required");
return switch (command) {
case InvokeCommand invokeCommand -> agentCommandService.handle(invokeCommand);
case ApprovalDecisionCommand approvalCommand -> approvalCommandService.decide(approvalCommand);
case CancelRunCommand cancelCommand -> runCommandService.cancel(cancelCommand);
case GetSessionCommand getSessionCommand -> queryService.getSession(getSessionCommand);
case GetRunCommand getRunCommand -> queryService.getRun(getRunCommand);
case GetLatestSessionRunCommand latestRunCommand -> queryService.getLatestSessionRun(latestRunCommand);
case ListSessionsCommand listSessionsCommand -> queryService.listSessions(listSessionsCommand);
case GetSessionTranscriptCommand transcriptCommand ->
queryService.getSessionTranscript(transcriptCommand);
case ListModelProviderOptionsCommand optionsCommand ->
modelProviderCommandService.listOptions(optionsCommand);
case ListModelProviderConfigurationsCommand configurationsCommand ->
modelProviderCommandService.listConfigurations(configurationsCommand);
case CreateModelProviderConfigurationCommand createCommand ->
modelProviderCommandService.createConfiguration(createCommand);
case UpdateModelProviderConfigurationCommand updateCommand ->
modelProviderCommandService.updateConfiguration(updateCommand);
case DeleteModelProviderConfigurationCommand deleteCommand ->
modelProviderCommandService.deleteConfiguration(deleteCommand);
case SwitchModelProviderCommand switchCommand ->
modelProviderCommandService.switchConfiguration(switchCommand);
};
}
}
@@ -0,0 +1,199 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.Builder;
import org.springframework.util.StringUtils;
/**
* Channel-neutral event emitted by the Gateway layer.
*/
@Builder
public record GatewayEvent(
GatewayEventType type,
String eventId,
String conversationId,
String sessionUid,
String runUid,
String itemId,
GatewayEventPayload payload,
Long timestamp) {
public GatewayEvent {
// GatewayEvent is the external event boundary; clients need stable identity and a concrete schema.
Objects.requireNonNull(type, "type is required");
if (!StringUtils.hasText(eventId)) {
throw new IllegalArgumentException("eventId is required");
}
Objects.requireNonNull(payload, "payload is required");
Objects.requireNonNull(timestamp, "timestamp is required");
if (timestamp < 0) {
throw new IllegalArgumentException("timestamp must not be negative");
}
}
/**
* Event categories exposed to channels.
*/
public enum GatewayEventType {
RUN_STARTED,
MESSAGE_STARTED,
MESSAGE_DELTA,
MESSAGE_COMPLETED,
TOOL_STARTED,
TOOL_COMPLETED,
INPUT_REQUESTED,
INPUT_COMPLETED,
APPROVAL_REQUESTED,
APPROVAL_COMPLETED,
RUN_STATUS,
RUN_COMPLETED,
ERROR
}
/**
* Marker boundary for structured event payloads.
*/
public sealed interface GatewayEventPayload permits
RunStartedPayload,
MessageStartedPayload,
MessageDeltaPayload,
MessageCompletedPayload,
ToolStartedPayload,
ToolCompletedPayload,
InputRequestedPayload,
InputCompletedPayload,
ApprovalRequestedPayload,
ApprovalCompletedPayload,
RunStatusPayload,
RunCompletedPayload,
ErrorPayload {
}
/** Run start payload. */
@Builder
public record RunStartedPayload(String traceId) implements GatewayEventPayload {
}
/** Assistant message start payload. */
@Builder
public record MessageStartedPayload(String traceId) implements GatewayEventPayload {
}
/** Assistant message delta payload. */
@Builder
public record MessageDeltaPayload(
String traceId,
Integer deltaIndex,
String delta) implements GatewayEventPayload {
}
/** Assistant message completion payload. */
@Builder
public record MessageCompletedPayload(String traceId) implements GatewayEventPayload {
}
/** Tool execution start payload. */
@Builder
public record ToolStartedPayload(
String traceId,
String toolName,
String toolCallId,
Map<String, Object> arguments,
String status) implements GatewayEventPayload {
}
/** Tool execution completion payload. */
@Builder
public record ToolCompletedPayload(
String traceId,
String toolName,
String toolCallId,
String approvalId,
String policyDecision,
String errorMessage,
Long elapsedMs,
String status) implements GatewayEventPayload {
}
/** Structured user input requested by a tool. */
@Builder
public record InputRequestedPayload(
String traceId,
String interactionId,
String targetTool,
String title,
String description,
List<Map<String, Object>> fields,
String status) implements GatewayEventPayload {
}
/** Completion of a structured user input request. */
@Builder
public record InputCompletedPayload(
String traceId,
String interactionId,
String status,
String errorMessage) implements GatewayEventPayload {
}
/** Approval request payload. */
@Builder
public record ApprovalRequestedPayload(
String traceId,
String toolName,
String approvalId,
String toolCallId,
String status) implements GatewayEventPayload {
}
/** Approval completion payload. */
@Builder
public record ApprovalCompletedPayload(
String traceId,
String toolName,
String approvalId,
String toolCallId,
String status) implements GatewayEventPayload {
}
/** Durable run snapshot emitted instead of starting a duplicate runtime. */
@Builder
public record RunStatusPayload(
String status,
String result,
String errorMessage,
boolean replayAvailable) implements GatewayEventPayload {
}
/** Run completion payload. */
@Builder
public record RunCompletedPayload(String traceId) implements GatewayEventPayload {
}
/** Gateway error payload. */
@Builder
public record ErrorPayload(
String traceId,
String errorMessage,
String status) implements GatewayEventPayload {
}
}
@@ -0,0 +1,184 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetLatestSessionRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionTranscriptCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListSessionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionListItem;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunListProjection;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunSnapshotService;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
import org.apache.hertzbeat.common.entity.agent.AgentTranscriptEntry;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
/**
* Gateway query commands. Query commands never invoke runtime.
*/
@Service
public class GatewayQueryService {
private final AgentSessionService sessionService;
private final AgentRunService runService;
private final AgentRunSnapshotService snapshotService;
public GatewayQueryService(AgentSessionService sessionService, AgentRunService runService,
AgentRunSnapshotService snapshotService) {
this.sessionService = sessionService;
this.runService = runService;
this.snapshotService = snapshotService;
}
public GatewaySingleResponse listSessions(ListSessionsCommand command) {
Page<AgentSession> sessions = sessionService.findSessions(
command.envelope(),
command.originEntryType(),
command.title(),
PageRequest.of(command.pageIndex(), command.pageSize()));
List<Long> sessionIds = sessions.stream().map(AgentSession::getId).toList();
Map<Long, AgentRunListProjection> latestRuns = runService.findLatestRunProjections(sessionIds);
List<AgentSessionListItem> projectedContent = sessions.stream()
.map(session -> AgentSessionListItem.from(session, latestRuns.get(session.getId())))
.sorted(Comparator.comparing(
AgentSessionListItem::gmtUpdate,
Comparator.nullsLast(Comparator.reverseOrder())))
.toList();
Page<AgentSessionListItem> projectedSessions = new PageImpl<>(
projectedContent, sessions.getPageable(), sessions.getTotalElements());
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.terminal(true)
.message("sessions")
.build())
.body(projectedSessions)
.events(List.of())
.build();
}
public GatewaySingleResponse getSession(GetSessionCommand command) {
return sessionService.findOwnedSession(
command.sessionUid(), command.envelope(), command.originEntryType())
.<GatewaySingleResponse>map(session -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(session.getSessionUid())
.terminal(true)
.message("session")
.build())
.body(session)
.events(List.of())
.build())
.orElseGet(() -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(command.sessionUid())
.terminal(true)
.message("Agent session not found")
.build())
.events(List.of())
.build());
}
public GatewaySingleResponse getRun(GetRunCommand command) {
return runService.findRun(command.runUid())
.filter(run -> command.originEntryType().name().equals(run.getEntryType()))
.flatMap(run -> sessionService.findOwnedSession(
String.valueOf(run.getSessionId()), command.envelope(), command.originEntryType())
.map(session -> snapshotService.snapshot(session, run)))
.<GatewaySingleResponse>map(snapshot -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(snapshot.sessionUid())
.runUid(snapshot.runUid())
.terminal(true)
.message("run")
.build())
.body(snapshot)
.events(List.of())
.build())
.orElseGet(() -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.runUid(command.runUid())
.terminal(true)
.message("Agent run not found")
.build())
.events(List.of())
.build());
}
public GatewaySingleResponse getLatestSessionRun(GetLatestSessionRunCommand command) {
return sessionService.findOwnedSession(
command.sessionUid(), command.envelope(), command.originEntryType())
.flatMap(session -> runService.findLatestRun(session.getId())
.filter(run -> command.originEntryType().name().equals(run.getEntryType()))
.map(run -> snapshotService.snapshot(session, run)))
.<GatewaySingleResponse>map(snapshot -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(snapshot.sessionUid())
.runUid(snapshot.runUid())
.terminal(true)
.message("run")
.build())
.body(snapshot)
.events(List.of())
.build())
.orElseGet(() -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(command.sessionUid())
.terminal(true)
.message("Agent run not found")
.build())
.events(List.of())
.build());
}
public GatewaySingleResponse getSessionTranscript(GetSessionTranscriptCommand command) {
PageRequest pageRequest = PageRequest.of(command.pageIndex(), command.pageSize());
AgentSession session = sessionService.findOwnedSession(
command.sessionUid(), command.envelope(), command.originEntryType()).orElse(null);
Page<AgentTranscriptEntry> transcript = session == null
? Page.empty(pageRequest)
: sessionService.findTranscriptEntries(session.getId(), pageRequest);
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(session == null ? command.sessionUid() : session.getSessionUid())
.terminal(true)
.message(session == null ? "Agent session not found" : "session-transcript")
.build())
.body(transcript)
.events(List.of())
.build();
}
}
@@ -0,0 +1,83 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.List;
import java.util.Objects;
import lombok.Builder;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
/**
* Channel-neutral Gateway response.
*/
public sealed interface GatewayResponse permits
GatewayResponse.GatewaySingleResponse,
GatewayResponse.GatewayStreamResponse {
Meta meta();
/**
* Final-only Gateway response.
*/
@Builder
record GatewaySingleResponse(
Meta meta,
Object body,
List<GatewayEvent> events) implements GatewayResponse {
public GatewaySingleResponse {
Objects.requireNonNull(meta, "meta is required");
events = events == null ? List.of() : List.copyOf(events);
}
}
/**
* Streaming Gateway response.
*/
@Builder
record GatewayStreamResponse(
Meta meta,
Flux<GatewayEvent> events) implements GatewayResponse {
public GatewayStreamResponse {
Objects.requireNonNull(meta, "meta is required");
Objects.requireNonNull(events, "events is required");
}
}
/**
* Common Gateway response metadata.
*/
@Builder
record Meta(
String commandId,
String conversationId,
String sessionUid,
String runUid,
boolean terminal,
String message) {
public Meta {
// Every response is correlated to one normalized command and exposes a human-readable outcome label.
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(message)) {
throw new IllegalArgumentException("Response command id and message are required");
}
}
}
}
@@ -0,0 +1,282 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ApprovalCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ApprovalRequestedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.MessageCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.InputRequestedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.InputCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.MessageDeltaPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.MessageStartedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.RunCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.RunStartedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ToolCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ToolStartedPayload;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent.RequestKind;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent.EventStatus;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeItemKind;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* Projects runtime facts into the external Gateway event contract.
*/
@Component
public class GatewayRuntimeEventProjector {
public GatewayEvent project(AgentRuntimeEvent event, String conversationId, String sessionUid, String runUid) {
// Runtime publishers only emit complete events; projection cannot represent a missing source event.
Objects.requireNonNull(event, "Runtime event is required");
return switch (event.getType()) {
case RUN_STARTED -> gatewayEvent(event, GatewayEventType.RUN_STARTED, conversationId, sessionUid, runUid,
RunStartedPayload.builder()
.traceId(event.getTraceId())
.build());
case ITEM_STARTED -> projectItemStarted(event, conversationId, sessionUid, runUid);
case ITEM_DELTA -> projectItemDelta(event, conversationId, sessionUid, runUid);
case ITEM_COMPLETED -> projectItemCompleted(event, conversationId, sessionUid, runUid);
case REQUESTED_ACTION -> projectRequestedAction(event, conversationId, sessionUid, runUid);
case REQUEST_COMPLETED -> projectRequestCompleted(event, conversationId, sessionUid, runUid);
case RUN_COMPLETED -> gatewayEvent(event, GatewayEventType.RUN_COMPLETED, conversationId, sessionUid,
runUid, RunCompletedPayload.builder()
.traceId(event.getTraceId())
.build());
case ERROR -> gatewayEvent(event, GatewayEventType.ERROR, conversationId, sessionUid, runUid,
errorPayload(event, event.getErrorMessage()));
};
}
private GatewayEvent projectItemStarted(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
if (isTool(event)) {
return gatewayEvent(event, GatewayEventType.TOOL_STARTED, conversationId, sessionUid, runUid,
ToolStartedPayload.builder()
.traceId(event.getTraceId())
.toolName(event.getToolName())
.toolCallId(event.getToolCallId())
.arguments(event.getToolArguments())
.status(event.getStatus() == null ? EventStatus.IN_PROGRESS.externalName()
: event.getStatus().externalName())
.build());
}
if (isAssistantMessage(event)) {
return gatewayEvent(event, GatewayEventType.MESSAGE_STARTED, conversationId, sessionUid, runUid,
MessageStartedPayload.builder()
.traceId(event.getTraceId())
.build());
}
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime item start event has no supported item kind.");
}
private GatewayEvent projectItemDelta(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
// Legacy runtime delta events without itemKind are assistant message deltas.
if (event.getItemKind() == null || isAssistantMessage(event)) {
return gatewayEvent(event, GatewayEventType.MESSAGE_DELTA, conversationId, sessionUid, runUid,
MessageDeltaPayload.builder()
.traceId(event.getTraceId())
.deltaIndex(event.getDeltaIndex())
.delta(event.getDelta())
.build());
}
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime item delta event has no supported item kind.");
}
private GatewayEvent projectItemCompleted(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
if (isTool(event)) {
return gatewayEvent(event, GatewayEventType.TOOL_COMPLETED, conversationId, sessionUid, runUid,
ToolCompletedPayload.builder()
.traceId(event.getTraceId())
.toolName(event.getToolName())
.toolCallId(event.getToolCallId())
.approvalId(event.getApprovalId())
.policyDecision(externalName(event.getPolicyDecision()))
.errorMessage(event.getErrorMessage())
.elapsedMs(event.getElapsedMs())
.status(toolCompletedStatus(event))
.build());
}
if (isAssistantMessage(event)) {
return gatewayEvent(event, GatewayEventType.MESSAGE_COMPLETED, conversationId, sessionUid, runUid,
MessageCompletedPayload.builder()
.traceId(event.getTraceId())
.build());
}
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime item completion event has no supported item kind.");
}
private GatewayEvent projectRequestedAction(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
if (event.getRequestKind() == RequestKind.USER_INPUT) {
Map<String, Object> requestPayload = event.getRequestPayload() == null
? Map.of() : event.getRequestPayload();
return gatewayEvent(event, GatewayEventType.INPUT_REQUESTED, conversationId, sessionUid, runUid,
InputRequestedPayload.builder()
.traceId(event.getTraceId())
.interactionId(event.getRequestId())
.targetTool(stringValue(requestPayload.get("targetTool")))
.title(stringValue(requestPayload.get("title")))
.description(stringValue(requestPayload.get("description")))
.fields(inputFields(requestPayload.get("fields")))
.status(event.getStatus().externalName())
.build());
}
if (event.getRequestKind() != RequestKind.APPROVAL) {
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime requested action event has no supported request kind.");
}
return gatewayEvent(event, GatewayEventType.APPROVAL_REQUESTED, conversationId, sessionUid, runUid,
ApprovalRequestedPayload.builder()
.traceId(event.getTraceId())
.toolName(event.getToolName())
.approvalId(event.getApprovalId())
.toolCallId(event.getToolCallId())
.status(event.getStatus() == null ? EventStatus.WAITING_APPROVAL.externalName()
: event.getStatus().externalName())
.build());
}
private GatewayEvent projectRequestCompleted(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
if (event.getRequestKind() == RequestKind.USER_INPUT) {
return gatewayEvent(event, GatewayEventType.INPUT_COMPLETED, conversationId, sessionUid, runUid,
InputCompletedPayload.builder()
.traceId(event.getTraceId())
.interactionId(event.getRequestId())
.status(event.getStatus().externalName())
.errorMessage(event.getErrorMessage())
.build());
}
if (event.getRequestKind() != RequestKind.APPROVAL) {
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime completed request event has no supported request kind.");
}
return gatewayEvent(event, GatewayEventType.APPROVAL_COMPLETED, conversationId, sessionUid, runUid,
ApprovalCompletedPayload.builder()
.traceId(event.getTraceId())
.toolName(event.getToolName())
.approvalId(event.getApprovalId())
.toolCallId(event.getToolCallId())
.status(event.getStatus().externalName())
.build());
}
private GatewayEvent unsupported(AgentRuntimeEvent event, String conversationId, String sessionUid, String runUid,
String message) {
return gatewayEvent(event, GatewayEventType.ERROR, conversationId, sessionUid, runUid,
errorPayload(event, message));
}
private GatewayEvent gatewayEvent(AgentRuntimeEvent event, GatewayEventType type, String conversationId,
String sessionUid, String runUid, GatewayEventPayload payload) {
return GatewayEvent.builder()
.type(type)
.eventId(eventId(event, type, runUid))
.conversationId(conversationId)
.sessionUid(sessionUid)
.runUid(runUid)
.itemId(event.getItemId())
.payload(payload)
.timestamp(timestamp(event.getTimestamp()))
.build();
}
private ErrorPayload errorPayload(AgentRuntimeEvent event, String errorMessage) {
return ErrorPayload.builder()
.traceId(event.getTraceId())
.errorMessage(errorMessage)
.status(event.getStatus() == null ? null : event.getStatus().externalName())
.build();
}
private String eventId(AgentRuntimeEvent event, GatewayEventType type, String runUid) {
String eventRunUid = StringUtils.hasText(runUid) ? runUid : "run";
if (event.getEventSequence() != null) {
return eventRunUid + ":event:" + event.getEventSequence();
}
String stableId = Stream.of(event.getRequestId(), event.getApprovalId(), event.getItemId(),
event.getToolCallId(), event.getTraceId())
.filter(StringUtils::hasText)
.findFirst()
.orElse("event");
String eventType = type.name().toLowerCase(Locale.ROOT);
return eventRunUid + ":" + eventType + ":" + stableId;
}
private String toolCompletedStatus(AgentRuntimeEvent event) {
if (event.getStatus() != null) {
return event.getStatus().externalName();
}
if (StringUtils.hasText(event.getErrorMessage())) {
return EventStatus.FAILED.externalName();
}
if (org.apache.hertzbeat.ai.gateway.tool.core.AgentPolicyDecision.REQUIRE_APPROVAL
== event.getPolicyDecision()) {
return EventStatus.WAITING_APPROVAL.externalName();
}
return EventStatus.COMPLETED.externalName();
}
private boolean isAssistantMessage(AgentRuntimeEvent event) {
return event.getItemKind() == AgentRuntimeItemKind.ASSISTANT_MESSAGE;
}
private boolean isTool(AgentRuntimeEvent event) {
return event.getItemKind() == AgentRuntimeItemKind.TOOL_CALL;
}
private Long timestamp(Instant timestamp) {
return timestamp == null ? System.currentTimeMillis() : timestamp.toEpochMilli();
}
private String externalName(Enum<?> value) {
return value == null ? null : value.name();
}
private String stringValue(Object value) {
return value instanceof String text ? text : null;
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> inputFields(Object value) {
if (!(value instanceof List<?> values)) {
return List.of();
}
return values.stream()
.filter(Map.class::isInstance)
.map(item -> Map.copyOf((Map<String, Object>) item))
.toList();
}
}
@@ -0,0 +1,153 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.List;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CreateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.DeleteModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderConfigurationsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderOptionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.SwitchModelProviderCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.UpdateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.application.ModelProviderConfigurationView.Provider;
import org.apache.hertzbeat.ai.gateway.runtime.ReloadableAgentRuntimeModelClient;
import org.apache.hertzbeat.ai.gateway.runtime.provider.AgentModelProviderRegistry;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfigState;
import org.apache.hertzbeat.manager.service.ModelProviderConfigurationService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* Channel-neutral model provider configuration commands.
*/
@Service
public class ModelProviderCommandService {
private final AgentModelProviderRegistry providerRegistry;
private final ModelProviderConfigurationService configurationService;
private final ReloadableAgentRuntimeModelClient runtimeModelClient;
public ModelProviderCommandService(AgentModelProviderRegistry providerRegistry,
ModelProviderConfigurationService configurationService,
ReloadableAgentRuntimeModelClient runtimeModelClient) {
this.providerRegistry = providerRegistry;
this.configurationService = configurationService;
this.runtimeModelClient = runtimeModelClient;
}
public GatewaySingleResponse listOptions(ListModelProviderOptionsCommand command) {
return response(command.commandId(), "model-provider-options", providerRegistry.options());
}
public GatewaySingleResponse listConfigurations(ListModelProviderConfigurationsCommand command) {
return stateResponse(command.commandId(), "model-provider-configurations",
configurationService.getState());
}
public GatewaySingleResponse createConfiguration(CreateModelProviderConfigurationCommand command) {
ModelProviderConfig config = copyConfiguration(command.config());
config.setUid(null);
providerRegistry.createModel(config);
return stateResponse(command.commandId(), "model-provider-configuration-created",
configurationService.createConfiguration(config));
}
public GatewaySingleResponse updateConfiguration(UpdateModelProviderConfigurationCommand command) {
ModelProviderConfig existing = configurationService.getConfiguration(command.providerUid());
ModelProviderConfig config = copyConfiguration(command.config());
config.setUid(command.providerUid());
boolean sameProvider = Objects.equals(existing.getCode(), config.getCode())
&& Objects.equals(existing.getType(), config.getType());
if (!StringUtils.hasText(config.getApiKey())
&& sameProvider) {
config.setApiKey(existing.getApiKey());
}
providerRegistry.createModel(config);
ModelProviderConfigState state = configurationService.updateConfiguration(
command.providerUid(), config);
if (Objects.equals(command.providerUid(), state.getActiveProviderUid())) {
runtimeModelClient.refreshConfiguration();
}
return stateResponse(command.commandId(), "model-provider-configuration-updated", state);
}
public GatewaySingleResponse deleteConfiguration(DeleteModelProviderConfigurationCommand command) {
boolean deletingActiveConfiguration = Objects.equals(
command.providerUid(), configurationService.getState().getActiveProviderUid());
ModelProviderConfigState state = configurationService.deleteConfiguration(command.providerUid());
if (deletingActiveConfiguration) {
runtimeModelClient.refreshConfiguration();
}
return stateResponse(command.commandId(), "model-provider-configuration-deleted", state);
}
public GatewaySingleResponse switchConfiguration(SwitchModelProviderCommand command) {
String previousProviderUid = configurationService.getState().getActiveProviderUid();
if (StringUtils.hasText(command.providerUid())) {
ModelProviderConfig config = configurationService.getConfiguration(command.providerUid());
providerRegistry.createModel(config);
}
ModelProviderConfigState state = configurationService.switchActiveConfiguration(command.providerUid());
if (!Objects.equals(previousProviderUid, state.getActiveProviderUid())) {
runtimeModelClient.refreshConfiguration();
}
return stateResponse(command.commandId(), "model-provider-configuration-switched", state);
}
private GatewaySingleResponse stateResponse(String commandId, String message,
ModelProviderConfigState state) {
List<Provider> providers = state.getProviders().stream()
.map(config -> new Provider(
config.getUid(),
config.getType(),
config.getCode(),
config.getBaseUrl(),
config.getModel(),
StringUtils.hasText(config.getApiKey())))
.toList();
return response(commandId, message,
new ModelProviderConfigurationView(state.getActiveProviderUid(), providers));
}
private GatewaySingleResponse response(String commandId, String message, Object body) {
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(commandId)
.terminal(true)
.message(message)
.build())
.body(body)
.events(List.of())
.build();
}
private ModelProviderConfig copyConfiguration(ModelProviderConfig source) {
ModelProviderConfig copy = new ModelProviderConfig();
copy.setUid(source.getUid());
copy.setType(source.getType());
copy.setCode(source.getCode());
copy.setBaseUrl(source.getBaseUrl());
copy.setModel(source.getModel());
copy.setApiKey(source.getApiKey());
return copy;
}
}
@@ -0,0 +1,57 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
/**
* Secret-free provider configuration state returned to Gateway channels.
*
* @param activeProviderUid active saved configuration UID; null selects the YAML default
* @param providers saved provider configurations without API keys
*/
@Schema(description = "Saved LLM provider configurations and active selection")
public record ModelProviderConfigurationView(
String activeProviderUid,
List<Provider> providers) {
public ModelProviderConfigurationView {
providers = List.copyOf(providers);
}
/**
* One secret-free saved provider configuration.
*
* @param uid stable saved configuration identifier
* @param type runtime provider implementation type
* @param code provider preset code
* @param baseUrl provider API endpoint
* @param model model identifier
* @param apiKeyConfigured whether a secret is persisted
*/
@Schema(description = "Saved LLM provider configuration without its API key")
public record Provider(
String uid,
String type,
String code,
String baseUrl,
String model,
boolean apiKeyConfigured) {
}
}
@@ -0,0 +1,164 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.application;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CancelRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunStatus;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.RunCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewayStreamResponse;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeControlRegistry;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionService;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
/**
* Run lifecycle commands. Cancellation never invokes runtime.
*/
@Service
public class RunCommandService {
private final AgentRunService runService;
private final AgentSessionService sessionService;
private final AgentRuntimeControlRegistry controlRegistry;
public RunCommandService(AgentRunService runService, AgentSessionService sessionService,
AgentRuntimeControlRegistry controlRegistry) {
this.runService = runService;
this.sessionService = sessionService;
this.controlRegistry = controlRegistry;
}
public GatewayResponse cancel(CancelRunCommand command) {
String runUid = requiredText(command.runUid(), "Agent run UID is required");
String reason = GatewayText.isBlank(command.reason())
? "Runtime cancellation requested."
: command.reason();
Optional<AgentRun> runOptional = runService.findRun(runUid);
if (runOptional.isEmpty()) {
GatewayEvent error = errorEvent(command.commandId(), null, runUid, "Agent run not found.");
return response(command, null, runUid, List.of(error));
}
AgentRun run = runOptional.get();
AgentSession session = session(run, command);
List<GatewayEvent> events;
if (session == null) {
events = List.of(errorEvent(command.commandId(), null, runUid, "Agent run not found."));
} else if (isTerminalRun(run)) {
events = List.of(errorEvent(command.commandId(), session, runUid,
"Agent run is already stopped."));
} else if (AgentRunStatus.RUNNING.name().equals(run.getStatus())) {
boolean cancelled = controlRegistry.cancel(runUid, reason);
events = cancelled ? List.of() : List.of(errorEvent(command.commandId(), session, runUid,
"Agent runtime run is not active or already stopped."));
} else {
AgentRun cancelledRun = runService.markCancelled(run, reason);
events = List.of(runCompletedEvent(command.commandId(), session, cancelledRun,
"Agent run cancelled."));
}
return response(command, session, runUid, events);
}
private GatewayResponse response(CancelRunCommand command, AgentSession session, String runUid,
List<GatewayEvent> events) {
if (command.replyMode() == ReplyMode.STREAM) {
return GatewayStreamResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(session == null ? null : session.getSessionUid())
.runUid(runUid)
.terminal(true)
.message("cancel")
.build())
.events(Flux.fromIterable(events))
.build();
}
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(session == null ? null : session.getSessionUid())
.runUid(runUid)
.terminal(true)
.message("cancel")
.build())
.body(Map.of("runUid", runUid))
.events(events)
.build();
}
private GatewayEvent runCompletedEvent(String commandId, AgentSession session, AgentRun run, String message) {
return GatewayEvent.builder()
.type(GatewayEventType.RUN_COMPLETED)
.eventId(commandId + ":run-completed")
.sessionUid(session == null ? null : session.getSessionUid())
.runUid(run == null ? null : run.getRunUid())
.payload(RunCompletedPayload.builder().build())
.timestamp(System.currentTimeMillis())
.build();
}
private GatewayEvent errorEvent(String commandId, AgentSession session, String runUid, String message) {
return GatewayEvent.builder()
.type(GatewayEventType.ERROR)
.eventId(commandId + ":error")
.sessionUid(session == null ? null : session.getSessionUid())
.runUid(runUid)
.payload(ErrorPayload.builder()
.errorMessage(message)
.build())
.timestamp(System.currentTimeMillis())
.build();
}
private AgentSession session(AgentRun run, CancelRunCommand command) {
if (run == null || run.getSessionId() == null) {
return null;
}
return sessionService.findOwnedSession(String.valueOf(run.getSessionId()), command.envelope(),
command.originEntryType()).orElse(null);
}
private boolean isTerminalRun(AgentRun run) {
String status = run == null ? null : run.getStatus();
return AgentRunStatus.SUCCEEDED.name().equals(status)
|| AgentRunStatus.FAILED.name().equals(status)
|| AgentRunStatus.CANCELLED.name().equals(status)
|| AgentRunStatus.RECOVERY_REQUIRED.name().equals(status);
}
private String requiredText(String value, String message) {
// Command identifiers originate in channel requests and may contain surrounding whitespace before lookup.
String normalized = GatewayText.normalize(value);
if (normalized == null) {
throw new IllegalArgumentException(message);
}
return normalized;
}
}
@@ -0,0 +1,35 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.channel.core;
/** Built-in Agent Gateway ingress channel identifiers. */
public enum ChannelId {
WEB_UI("web-ui"),
SYSTEM("system");
private final String id;
ChannelId(String id) {
this.id = id;
}
public String id() {
return id;
}
}
@@ -0,0 +1,174 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.channel.webui;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CreateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.DeleteModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderConfigurationsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderOptionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.SwitchModelProviderCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.UpdateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommandRouter;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.ModelProviderConfigurationView;
import org.apache.hertzbeat.ai.gateway.channel.core.ChannelId;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.identity.ActorSupport;
import org.apache.hertzbeat.ai.gateway.runtime.provider.AgentModelProviderOption;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Agent model provider discovery and configuration API.
*/
@Tag(name = "Agent Model Provider API")
@RestController
@RequestMapping(path = "/api/agent/model-providers", produces = {APPLICATION_JSON_VALUE})
public class ModelProviderConfigController {
private final GatewayCommandRouter commandRouter;
public ModelProviderConfigController(GatewayCommandRouter commandRouter) {
this.commandRouter = commandRouter;
}
@GetMapping("/options")
@Operation(summary = "List registered Agent model provider configuration options")
public ResponseEntity<Message<List<AgentModelProviderOption>>> getOptions() {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
ListModelProviderOptionsCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("list-model-provider-options")
.build());
@SuppressWarnings("unchecked")
List<AgentModelProviderOption> options = (List<AgentModelProviderOption>) response.body();
return ResponseEntity.ok(Message.success(options));
}
@GetMapping("/configurations")
@Operation(summary = "List saved Agent model provider configurations")
public ResponseEntity<Message<ModelProviderConfigurationView>> getConfigurations() {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
ListModelProviderConfigurationsCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("list-model-provider-configurations")
.build());
return stateResponse(response);
}
@PostMapping("/configurations")
@Operation(summary = "Validate and create an Agent model provider configuration")
public ResponseEntity<Message<ModelProviderConfigurationView>> createConfiguration(
@RequestBody ModelProviderConfig config) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
CreateModelProviderConfigurationCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("create-model-provider-configuration")
.config(config)
.build());
return stateResponse(response);
}
@PutMapping("/configurations/{providerUid}")
@Operation(summary = "Validate and update an Agent model provider configuration")
public ResponseEntity<Message<ModelProviderConfigurationView>> updateConfiguration(
@PathVariable String providerUid,
@RequestBody ModelProviderConfig config) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
UpdateModelProviderConfigurationCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("update-model-provider-configuration")
.providerUid(providerUid)
.config(config)
.build());
return stateResponse(response);
}
@DeleteMapping("/configurations/{providerUid}")
@Operation(summary = "Delete an Agent model provider configuration")
public ResponseEntity<Message<ModelProviderConfigurationView>> deleteConfiguration(
@PathVariable String providerUid) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
DeleteModelProviderConfigurationCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("delete-model-provider-configuration")
.providerUid(providerUid)
.build());
return stateResponse(response);
}
@PutMapping("/active/{providerUid}")
@Operation(summary = "Switch the active Agent model provider configuration")
public ResponseEntity<Message<ModelProviderConfigurationView>> switchConfiguration(
@PathVariable String providerUid) {
return switchConfiguration(providerUid, "switch-model-provider-configuration");
}
@DeleteMapping("/active")
@Operation(summary = "Switch the active Agent model provider to the YAML default")
public ResponseEntity<Message<ModelProviderConfigurationView>> switchToDefault() {
return switchConfiguration(null, "switch-model-provider-to-default");
}
private ResponseEntity<Message<ModelProviderConfigurationView>> switchConfiguration(
String providerUid, String commandId) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
SwitchModelProviderCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId(commandId)
.providerUid(providerUid)
.build());
return stateResponse(response);
}
private ResponseEntity<Message<ModelProviderConfigurationView>> stateResponse(
GatewaySingleResponse response) {
return ResponseEntity.ok(Message.success((ModelProviderConfigurationView) response.body()));
}
private GatewayEnvelope webUiEnvelope() {
return GatewayEnvelope.builder()
.channelId(ChannelId.WEB_UI.id())
.receivedAt(System.currentTimeMillis())
.actor(ActorSupport.requireCurrentAdminSurenessActor())
.workspaceId(AuthTokenRequestContext.currentWorkspaceId())
.build();
}
}
@@ -0,0 +1,221 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.channel.webui;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.hertzbeat.ai.gateway.channel.core.ChannelId;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommandRouter;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetLatestSessionRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionTranscriptCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListSessionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.identity.ActorSupport;
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunSnapshot;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionListItem;
import org.apache.hertzbeat.common.entity.agent.AgentTranscriptEntry;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Read-only Agent Gateway query API.
*/
@Tag(name = "Agent Query API")
@RestController
@RequestMapping(path = "/api/agent", produces = {APPLICATION_JSON_VALUE})
public class QueryController {
private final GatewayCommandRouter commandRouter;
public QueryController(GatewayCommandRouter commandRouter) {
this.commandRouter = commandRouter;
}
@GetMapping("/sessions/{sessionId}")
@Operation(summary = "Get Agent Gateway session")
public ResponseEntity<Message<GatewaySingleResponse>> getSession(
@Parameter(description = "Session ID or UID") @PathVariable String sessionId) {
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(
GetSessionCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-session:" + sessionId)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.sessionUid(sessionId)
.build())));
}
@GetMapping("/runs/{runUid}")
@Operation(summary = "Get an owned Agent Gateway run")
public ResponseEntity<Message<AgentRunSnapshot>> getRun(
@Parameter(description = "Run UID") @PathVariable String runUid) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
GetRunCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-run:" + runUid)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.runUid(runUid)
.build());
return ResponseEntity.ok(Message.success((AgentRunSnapshot) response.body()));
}
@GetMapping("/sessions/{sessionUid}/latest-run")
@Operation(summary = "Get the latest owned Agent Gateway run for a session")
public ResponseEntity<Message<AgentRunSnapshot>> getLatestSessionRun(
@Parameter(description = "Session UID") @PathVariable String sessionUid) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
GetLatestSessionRunCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-latest-run:" + sessionUid)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.sessionUid(sessionUid)
.build());
return ResponseEntity.ok(Message.success((AgentRunSnapshot) response.body()));
}
@GetMapping("/sessions")
@Operation(summary = "List current WebUI user's Agent Gateway sessions")
@SuppressWarnings("unchecked")
public ResponseEntity<Message<Page<AgentSessionListItem>>> listSessions(
@RequestParam(defaultValue = "0") int pageIndex,
@RequestParam(defaultValue = "50") int pageSize) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
ListSessionsCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("list-sessions:" + pageIndex)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.title(null)
.pageIndex(pageIndex)
.pageSize(pageSize)
.build());
return ResponseEntity.ok(Message.success((Page<AgentSessionListItem>) response.body()));
}
@GetMapping("/alert-analysis/sessions")
@Operation(summary = "List automatic alert analysis sessions")
@SuppressWarnings("unchecked")
public ResponseEntity<Message<Page<AgentSessionListItem>>> listAlertAnalysisSessions(
@RequestParam(defaultValue = "0") int pageIndex,
@RequestParam(defaultValue = "50") int pageSize,
@RequestParam(required = false) String search) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
ListSessionsCommand.builder()
.envelope(alertAnalysisEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("list-alert-analysis-sessions:" + pageIndex)
.originEntryType(AgentRuntimeEntryType.ALERT_TRIGGER)
.title(search)
.pageIndex(pageIndex)
.pageSize(pageSize)
.build());
return ResponseEntity.ok(Message.success((Page<AgentSessionListItem>) response.body()));
}
@GetMapping("/alert-analysis/sessions/{sessionId}")
@Operation(summary = "Get automatic alert analysis session")
public ResponseEntity<Message<GatewaySingleResponse>> getAlertAnalysisSession(
@Parameter(description = "Session ID or UID") @PathVariable String sessionId) {
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(
GetSessionCommand.builder()
.envelope(alertAnalysisEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-alert-analysis-session:" + sessionId)
.originEntryType(AgentRuntimeEntryType.ALERT_TRIGGER)
.sessionUid(sessionId)
.build())));
}
@GetMapping("/sessions/{sessionUid}/transcript")
@Operation(summary = "List Agent Gateway session transcript entries")
@SuppressWarnings("unchecked")
public ResponseEntity<Message<Page<AgentTranscriptEntry>>> listSessionTranscript(
@Parameter(description = "Session UID or numeric ID") @PathVariable String sessionUid,
@RequestParam(defaultValue = "0") int pageIndex,
@RequestParam(defaultValue = "50") int pageSize) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
GetSessionTranscriptCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-session-transcript:" + sessionUid)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.sessionUid(sessionUid)
.pageIndex(pageIndex)
.pageSize(pageSize)
.build());
return ResponseEntity.ok(Message.success((Page<AgentTranscriptEntry>) response.body()));
}
@GetMapping("/alert-analysis/sessions/{sessionUid}/transcript")
@Operation(summary = "List automatic alert analysis session transcript entries")
@SuppressWarnings("unchecked")
public ResponseEntity<Message<Page<AgentTranscriptEntry>>> listAlertAnalysisSessionTranscript(
@Parameter(description = "Session UID or numeric ID") @PathVariable String sessionUid,
@RequestParam(defaultValue = "0") int pageIndex,
@RequestParam(defaultValue = "50") int pageSize) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
GetSessionTranscriptCommand.builder()
.envelope(alertAnalysisEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-alert-analysis-session-transcript:" + sessionUid)
.originEntryType(AgentRuntimeEntryType.ALERT_TRIGGER)
.sessionUid(sessionUid)
.pageIndex(pageIndex)
.pageSize(pageSize)
.build());
return ResponseEntity.ok(Message.success((Page<AgentTranscriptEntry>) response.body()));
}
private GatewayEnvelope webUiEnvelope() {
return GatewayEnvelope.builder()
.channelId(ChannelId.WEB_UI.id())
.receivedAt(System.currentTimeMillis())
.actor(ActorSupport.requireCurrentSurenessActor())
.workspaceId(AuthTokenRequestContext.currentWorkspaceId())
.build();
}
private GatewayEnvelope alertAnalysisEnvelope() {
ActorSupport.requireCurrentAdminSurenessActor();
return GatewayEnvelope.builder()
.channelId(ChannelId.SYSTEM.id())
.receivedAt(System.currentTimeMillis())
.actor(AgentActor.alertAnalysisActor())
.workspaceId(AuthTokenScopes.DEFAULT_WORKSPACE_ID)
.build();
}
}
@@ -0,0 +1,217 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.channel.webui;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ApprovalDecisionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CancelRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommandRouter;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.FailureKind;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.TargetCanonicalizationException;
import org.apache.hertzbeat.ai.gateway.channel.core.ChannelId;
import org.apache.hertzbeat.ai.gateway.channel.webui.dto.WebUiChatStreamRequest;
import org.apache.hertzbeat.ai.gateway.contract.AgentResponseLanguage;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewayStreamResponse;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
import org.apache.hertzbeat.ai.gateway.identity.ActorSupport;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.tool.core.AgentApprovalDecision;
import org.apache.hertzbeat.ai.gateway.tool.interaction.AgentInteractionInputService;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
/**
* Interactive Agent Gateway endpoints used by the WebUI.
*/
@Tag(name = "Agent WebUI API")
@RestController
@RequestMapping(path = "/api/agent", produces = {APPLICATION_JSON_VALUE})
public class WebUiController {
private final GatewayCommandRouter commandRouter;
private final AgentInteractionInputService interactionInputService;
public WebUiController(GatewayCommandRouter commandRouter,
AgentInteractionInputService interactionInputService) {
this.commandRouter = commandRouter;
this.interactionInputService = interactionInputService;
}
@PostMapping("/webui/chat")
@Operation(summary = "Send a WebUI chat message")
public ResponseEntity<Message<GatewaySingleResponse>> chat(
@Valid @RequestBody WebUiChatStreamRequest request,
@RequestHeader(name = HttpHeaders.ACCEPT_LANGUAGE, required = false) String acceptLanguage) {
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(
chatCommand(request, ReplyMode.FINAL_ONLY, acceptLanguage))));
}
@PostMapping(value = "/webui/chat/stream", produces = TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Stream a WebUI chat message")
public Flux<ServerSentEvent<GatewayEvent>> streamChat(
@Valid @RequestBody WebUiChatStreamRequest request,
@RequestHeader(name = HttpHeaders.ACCEPT_LANGUAGE, required = false) String acceptLanguage) {
InvokeCommand command = chatCommand(request, ReplyMode.STREAM, acceptLanguage);
return Flux.defer(() -> ((GatewayStreamResponse) commandRouter.handle(command)).events())
.map(this::toServerSentEvent)
.onErrorResume(exception -> Flux.just(toServerSentEvent(errorEvent(exception))));
}
@PostMapping("/runs/{runUid}/stop")
@Operation(summary = "Stop an Agent Gateway run")
public ResponseEntity<Message<GatewaySingleResponse>> stopRun(
@Parameter(description = "Run UID") @PathVariable String runUid) {
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(
CancelRunCommand.builder()
.envelope(envelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("stop-run:" + runUid)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.runUid(runUid)
.reason("Stopped by the WebUI user.")
.build())));
}
@PostMapping("/approvals/{approvalId}/approve")
@Operation(summary = "Approve Agent Gateway approval")
public ResponseEntity<Message<GatewaySingleResponse>> approve(
@Parameter(description = "Approval ID") @PathVariable String approvalId) {
ApprovalDecisionCommand command = ApprovalDecisionCommand.builder()
.envelope(envelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId(approvalId)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.approvalId(approvalId)
.decision(AgentApprovalDecision.APPROVED)
.build();
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(command)));
}
@PostMapping("/approvals/{approvalId}/reject")
@Operation(summary = "Reject Agent Gateway approval")
public ResponseEntity<Message<GatewaySingleResponse>> reject(
@Parameter(description = "Approval ID") @PathVariable String approvalId) {
ApprovalDecisionCommand command = ApprovalDecisionCommand.builder()
.envelope(envelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId(approvalId)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.approvalId(approvalId)
.decision(AgentApprovalDecision.REJECTED)
.build();
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(command)));
}
@PostMapping("/interactions/{interactionId}/submit")
@Operation(summary = "Submit values requested by an Agent interaction tool")
public ResponseEntity<Message<String>> submitInteraction(
@Parameter(description = "Interaction ID") @PathVariable String interactionId,
@RequestBody InteractionSubmission submission) {
GatewayEnvelope envelope = envelope();
interactionInputService.submit(interactionId, envelope.getActor(), envelope.getWorkspaceId(),
submission == null ? Map.of() : submission.values());
return ResponseEntity.ok(Message.success("submitted"));
}
private InvokeCommand chatCommand(WebUiChatStreamRequest request, ReplyMode replyMode, String acceptLanguage) {
// WebUI attachments are optional; commands expose an immutable collection to downstream services.
List<String> attachments = request.getAttachments() == null
? List.of()
: List.copyOf(request.getAttachments());
UserInput input = UserInput.builder()
.messageId(request.getMessageId())
.conversationId(request.getConversationId())
.target(request.getTarget())
.message(UserInput.Message.builder()
.text(request.getMessage())
.attachments(attachments)
.build())
.build();
return InvokeCommand.builder()
.envelope(envelope().toBuilder()
.preferredLanguage(AgentResponseLanguage.fromAcceptLanguage(acceptLanguage))
.build())
.replyMode(replyMode)
.commandId(input.getMessageId())
.userInput(input)
.entryType(AgentRuntimeEntryType.USER_INPUT)
.build();
}
private GatewayEnvelope envelope() {
return GatewayEnvelope.builder()
.channelId(ChannelId.WEB_UI.id())
.receivedAt(System.currentTimeMillis())
.actor(ActorSupport.requireCurrentSurenessActor())
.workspaceId(AuthTokenRequestContext.currentWorkspaceId())
.build();
}
private ServerSentEvent<GatewayEvent> toServerSentEvent(GatewayEvent item) {
return ServerSentEvent.builder(item)
.event(item.type().name().toLowerCase(Locale.ROOT))
.id(item.eventId())
.build();
}
private GatewayEvent errorEvent(Throwable failure) {
if (failure instanceof TargetCanonicalizationException canonicalization) {
boolean mismatch = canonicalization.kind() == FailureKind.MISMATCH;
return new GatewayEvent(GatewayEventType.ERROR, "webui:error", null, null, null, null,
new ErrorPayload(null, mismatch ? "Investigation target does not match"
: "Investigation target is unavailable",
mismatch ? "TARGET_MISMATCH" : "TARGET_UNAVAILABLE"), System.currentTimeMillis());
}
return new GatewayEvent(GatewayEventType.ERROR, "webui:error", null, null, null, null,
new ErrorPayload(null, "Agent Gateway stream failed", null), System.currentTimeMillis());
}
/** Values supplied to a pending interaction request. */
public record InteractionSubmission(Map<String, Object> values) {
public InteractionSubmission {
values = values == null ? Map.of() : Map.copyOf(values);
}
}
}
@@ -0,0 +1,70 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.channel.webui.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
/**
* WebUI chat stream request.
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WebUiChatStreamRequest {
/**
* Stable conversation ID used to locate the Gateway session.
*/
@NotBlank
@Size(max = 256)
private String conversationId;
/**
* Stable message ID used to locate the Gateway run on retries.
*/
@NotBlank
@Size(max = 128)
private String messageId;
/**
* User input text.
*/
@NotBlank
@Size(max = 8192)
private String message;
/**
* Optional target reference.
*/
@Valid
private AgentTargetRef target;
/**
* Optional attachment references.
*/
private List<String> attachments;
}
@@ -0,0 +1,52 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.contract;
import jakarta.validation.constraints.Size;
import java.util.List;
import java.util.Objects;
import lombok.Builder;
/**
* Trusted, non-persistent identifiers describing one automatically correlated alert incident.
*/
@Builder
public record AgentAlertIncidentContext(
Long analysisPolicyId,
Long triggerAlertId,
@Size(max = 256) List<Long> alertIds,
int alertCount,
long windowStartedAt) {
public AgentAlertIncidentContext {
analysisPolicyId = Objects.requireNonNull(analysisPolicyId, "Analysis policy id is required");
alertIds = alertIds == null ? List.of() : List.copyOf(alertIds);
if (alertIds.size() > 256) {
throw new IllegalArgumentException("Alert incident supports at most 256 alert ids");
}
if (alertIds.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("Alert incident ids must not contain null values");
}
if (alertCount <= 0) {
throw new IllegalArgumentException("Alert incident count must be positive");
}
if (windowStartedAt < 0) {
throw new IllegalArgumentException("Alert incident window start must not be negative");
}
}
}
@@ -0,0 +1,99 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.contract;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Exact non-live Log Explore page scope selected by an operator. */
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class AgentLogRef {
@Positive
private Long start;
@Positive
private Long end;
@Pattern(regexp = "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
private String traceId;
@Pattern(regexp = "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
private String spanId;
@Min(1)
@Max(24)
private Integer severityNumber;
@Pattern(regexp = "TRACE|DEBUG|INFO|WARN|ERROR|FATAL")
private String severityText;
@Size(max = 256)
private String search;
@Size(max = 512)
private String serviceName;
@Size(max = 512)
private String serviceNamespace;
@Size(max = 512)
private String environment;
@Size(max = 2048)
private String resourceFilter;
@Size(max = 2048)
private String attributeFilter;
@NotNull
private Boolean hideInternal;
@NotNull
private Boolean hideNoise;
@NotNull
@PositiveOrZero
@Max(10_000)
private Integer pageIndex;
@NotNull
@Min(1)
@Max(100)
private Integer pageSize;
@AssertTrue
@JsonIgnore
public boolean isTimeWindowValid() {
return start != null && end != null && start < end;
}
}
@@ -0,0 +1,60 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.contract;
import java.util.List;
import java.util.Locale;
import org.springframework.util.StringUtils;
/**
* Resolves model response languages against the locales supported by the HertzBeat WebUI.
*/
public final class AgentResponseLanguage {
public static final String DEFAULT = "en-US";
private static final List<Locale> SUPPORTED = List.of(
Locale.forLanguageTag("en-US"),
Locale.forLanguageTag("zh-CN"),
Locale.forLanguageTag("zh-TW"),
Locale.forLanguageTag("ja-JP"),
Locale.forLanguageTag("pt-BR"));
private AgentResponseLanguage() {
}
public static String fromAcceptLanguage(String acceptLanguage) {
if (StringUtils.hasText(acceptLanguage)) {
try {
Locale matched = Locale.lookup(Locale.LanguageRange.parse(acceptLanguage), SUPPORTED);
if (matched != null) {
return matched.toLanguageTag();
}
} catch (IllegalArgumentException ignored) {
// Malformed external headers fall back to the configured system locale.
}
}
return systemDefault();
}
public static String systemDefault() {
Locale matched = Locale.lookup(
Locale.LanguageRange.parse(Locale.getDefault().toLanguageTag()), SUPPORTED);
return matched == null ? DEFAULT : matched.toLanguageTag();
}
}
@@ -0,0 +1,43 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.contract;
import java.util.List;
import lombok.Builder;
/** Versioned execution material persisted on the authoritative USER transcript entry. */
@Builder(toBuilder = true)
public record AgentRunRequestSnapshot(
String version,
String conversationId,
String messageId,
String entryType,
AgentTargetRef target,
AgentAlertIncidentContext alertIncident,
String message,
List<String> attachments,
String preferredLanguage,
String approvalHandling,
String replyMode) {
public static final String VERSION = "1";
public AgentRunRequestSnapshot {
attachments = attachments == null ? List.of() : List.copyOf(attachments);
}
}
@@ -0,0 +1,43 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.contract;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Canonical service identity exported from persisted entity identities. */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AgentServiceRef {
@NotBlank
@Size(max = 512)
private String name;
@Size(max = 512)
private String namespace;
@Size(max = 512)
private String environment;
}
@@ -0,0 +1,87 @@
/*
* 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.
*/
package org.apache.hertzbeat.ai.gateway.contract;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.time.DateTimeException;
import java.time.ZoneId;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Signal query and time-window context selected by an operator.
*/
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class AgentSignalRef {
@NotBlank
@Pattern(regexp = "metrics|logs|traces")
private String type;
@Size(max = 2048)
private String query;
@Size(max = 64)
private String timeRange;
@PositiveOrZero
private Long start;
@PositiveOrZero
private Long end;
@Size(max = 64)
private String timezone;
/**
* Absolute windows must be complete and ordered; relative-only windows leave both boundaries absent.
*/
@AssertTrue
@JsonIgnore
public boolean isAbsoluteWindowValid() {
if (start == null && end == null) {
return true;
}
return start != null && end != null && start < end;
}
/** A supplied timezone must be a real IANA or fixed-offset zone. */
@AssertTrue
@JsonIgnore
public boolean isTimezoneValid() {
if (timezone == null) {
return true;
}
try {
ZoneId.of(timezone);
return true;
} catch (DateTimeException ignored) {
return false;
}
}
}

Some files were not shown because too many files have changed in this diff Show More