mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
1867
Commits
fix-chinese
...
2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40e7485be1 | ||
|
|
b4ca58e57b | ||
|
|
e1810a45f8 | ||
|
|
a7b26c6b66 | ||
|
|
d6a801e36f | ||
|
|
34e05a05ce | ||
|
|
1f73694892 | ||
|
|
60a682a223 | ||
|
|
2c5754302c | ||
|
|
d40787953d | ||
|
|
af2e534dfb | ||
|
|
fd6f5590bf | ||
|
|
906f4283b6 | ||
|
|
cc46063bea | ||
|
|
3398db3c71 | ||
|
|
b84cb3a73a | ||
|
|
fcdcfe2428 | ||
|
|
0db1e44f4a | ||
|
|
da3d2a4e64 | ||
|
|
de1850a6e8 | ||
|
|
f82c340a16 | ||
|
|
b8ad16b74c | ||
|
|
720d20bdc4 | ||
|
|
4378c83039 | ||
|
|
e1d8eb0627 | ||
|
|
4200ffab34 | ||
|
|
c9c310d8f6 | ||
|
|
284cab8544 | ||
|
|
2b16b47a0a | ||
|
|
4172c862a6 | ||
|
|
1c35254ff7 | ||
|
|
9a2617ff0e | ||
|
|
842775b3df | ||
|
|
c831cfd146 | ||
|
|
0fda5be2bd | ||
|
|
7d18c85dc4 | ||
|
|
82f2440fd0 | ||
|
|
ce05942c77 | ||
|
|
a7e94a4b38 | ||
|
|
d83bbd7486 | ||
|
|
0291c46bec | ||
|
|
2e425706f8 | ||
|
|
873ed9dc5d | ||
|
|
b38ebb96b2 | ||
|
|
8833d8a146 | ||
|
|
c5a202a842 | ||
|
|
5c126ea8f7 | ||
|
|
ec51227a8c | ||
|
|
00f91d4433 | ||
|
|
d4b1db08f7 | ||
|
|
1eb35646d7 | ||
|
|
7d7e396a41 | ||
|
|
03946f3c8a | ||
|
|
021cbb176f | ||
|
|
9217f669ad | ||
|
|
420d280e5d | ||
|
|
54be0dfde0 | ||
|
|
cc95d4a9ad | ||
|
|
ae4c15d490 | ||
|
|
5b4f18fdbc | ||
|
|
27633afb26 | ||
|
|
64a153f8e8 | ||
|
|
649c6413f0 | ||
|
|
65a8026baa | ||
|
|
ffa2adbc91 | ||
|
|
f0e016beb1 | ||
|
|
f753f21c22 | ||
|
|
c83565b818 | ||
|
|
7114c0e71b | ||
|
|
5fb86ee9b0 | ||
|
|
bb3aef3ecf | ||
|
|
9ba2f5cd17 | ||
|
|
5046236b82 | ||
|
|
d74564cc88 | ||
|
|
6f8258cc7d | ||
|
|
8efbb02603 | ||
|
|
4322c0d21d | ||
|
|
7dffb24c4b | ||
|
|
f5b4d7401c | ||
|
|
7afb6501e7 | ||
|
|
c4190b4c0a | ||
|
|
aa3704bd73 | ||
|
|
6588f08563 | ||
|
|
9c40cd466c | ||
|
|
6bc71db9f1 | ||
|
|
dd3039611c | ||
|
|
2d2a423c79 | ||
|
|
d0b4c194ea | ||
|
|
b0ba220883 | ||
|
|
fe70a592b9 | ||
|
|
293b983aa4 | ||
|
|
100ebf9ab5 | ||
|
|
b7c3628cbd | ||
|
|
a09e0cdc33 | ||
|
|
ad45b32881 | ||
|
|
5294816012 | ||
|
|
50b17faa55 | ||
|
|
134ffc61e7 | ||
|
|
b8b1734e23 | ||
|
|
ea09f43580 | ||
|
|
a6cadbc7b7 | ||
|
|
2702c55e73 | ||
|
|
7d0b84a20c | ||
|
|
e6a45c721c | ||
|
|
6dd503e83b | ||
|
|
61f5dd325c | ||
|
|
865fcc6a9d | ||
|
|
a866b0f97b | ||
|
|
2d6d146fc5 | ||
|
|
6e63b80e72 | ||
|
|
697c36e20c | ||
|
|
1e0bcc6547 | ||
|
|
801ff3ad02 | ||
|
|
57548d9d08 | ||
|
|
ca39b5ec08 | ||
|
|
4f6fd62a6c | ||
|
|
e20f99d9ac | ||
|
|
733bc5e93f | ||
|
|
70c9034b89 | ||
|
|
410161c3f4 | ||
|
|
5a399175cb | ||
|
|
29ac811916 | ||
|
|
3c79e5c31e | ||
|
|
db8f745d74 | ||
|
|
b4ff84110b | ||
|
|
5eccabbfd4 | ||
|
|
c179e270c9 | ||
|
|
3204a4525e | ||
|
|
7cc18f64f3 | ||
|
|
d579755cd9 | ||
|
|
4ae6f14801 | ||
|
|
1788fcb069 | ||
|
|
74a699acad | ||
|
|
b2c2335aa0 | ||
|
|
209ef4c2a8 | ||
|
|
b64ae04aa0 | ||
|
|
b62f10c14a | ||
|
|
63051798a8 | ||
|
|
5694772e9f | ||
|
|
54d8296a5f | ||
|
|
4ae8fc8599 | ||
|
|
9824570911 | ||
|
|
5b5217373a | ||
|
|
7763124a06 | ||
|
|
1a8a80186e | ||
|
|
c2099f64fd | ||
|
|
249687c97c | ||
|
|
f4deccec03 | ||
|
|
779b0c87ce | ||
|
|
f83dbe382e | ||
|
|
8976255d0c | ||
|
|
a264e8343a | ||
|
|
2d2705d985 | ||
|
|
a07b84523f | ||
|
|
3ab089a7c9 | ||
|
|
c997a608f0 | ||
|
|
8b0b690651 | ||
|
|
7bd7603d33 | ||
|
|
7ce055d2ce | ||
|
|
68c70a9b39 | ||
|
|
e1e47b84aa | ||
|
|
2e10ca3aba | ||
|
|
90ba362cda | ||
|
|
ab04c63b4c | ||
|
|
e12e774a23 | ||
|
|
08960234e4 | ||
|
|
9e2e957282 | ||
|
|
d92d11a5d9 | ||
|
|
4685388063 | ||
|
|
e8fcaf4a68 | ||
|
|
607f6fc1d1 | ||
|
|
e10a107d78 | ||
|
|
3a5160379b | ||
|
|
85b5a54aeb | ||
|
|
8b0564dead | ||
|
|
5ee9a73b31 | ||
|
|
a24e0a5aef | ||
|
|
ca72913d00 | ||
|
|
faa092476e | ||
|
|
66b7761278 | ||
|
|
3f6117cfd8 | ||
|
|
84e4491571 | ||
|
|
5f35d4d0f2 | ||
|
|
0c9afd013c | ||
|
|
ccee0abcb1 | ||
|
|
e5093765f1 | ||
|
|
2a78de58f6 | ||
|
|
989838b370 | ||
|
|
937acf0d47 | ||
|
|
c11ada62e0 | ||
|
|
3323fe0a7e | ||
|
|
e73ab67194 | ||
|
|
a4ecbb8552 | ||
|
|
c3a12b946d | ||
|
|
da776d95db | ||
|
|
3d6aad4b20 | ||
|
|
1ef38b2297 | ||
|
|
a25c8d85b8 | ||
|
|
80df634bb0 | ||
|
|
abfb9c804a | ||
|
|
05f0b2a4d4 | ||
|
|
fb1a1ae117 | ||
|
|
bf1e90e803 | ||
|
|
31eae17414 | ||
|
|
893197a760 | ||
|
|
b99c22e5df | ||
|
|
afb80d5f66 | ||
|
|
ca9e8937b7 | ||
|
|
56d83f071b | ||
|
|
429899321f | ||
|
|
05d4426614 | ||
|
|
8cfbd4c287 | ||
|
|
5b65ebd196 | ||
|
|
8da89c6764 | ||
|
|
8edb5e0fa0 | ||
|
|
27ceea7a2d | ||
|
|
411b9b2d7a | ||
|
|
29f6f9b904 | ||
|
|
b1f906cbf9 | ||
|
|
1f661e8bc5 | ||
|
|
2f861a4663 | ||
|
|
eb91c779c8 | ||
|
|
0227997f7b | ||
|
|
446faec7f6 | ||
|
|
9a983d5b4f | ||
|
|
90e784e199 | ||
|
|
a4c654e14f | ||
|
|
dc99424924 | ||
|
|
f04294279d | ||
|
|
4a66fd2f91 | ||
|
|
d3e62bcaef | ||
|
|
859eb678d4 | ||
|
|
48ab70f71e | ||
|
|
81d55cc925 | ||
|
|
2cc5ef78a4 | ||
|
|
91a293b46d | ||
|
|
97b05e06d8 | ||
|
|
93707948ff | ||
|
|
80f390d2bf | ||
|
|
184e8f71dd | ||
|
|
96f16f3dfc | ||
|
|
ac4e677e01 | ||
|
|
37f8ababc9 | ||
|
|
d3d338da11 | ||
|
|
b2377e90ed | ||
|
|
a34cc4b7e6 | ||
|
|
4a8239f132 | ||
|
|
0a2a2b0102 | ||
|
|
a4114eadbc | ||
|
|
5efb02a018 | ||
|
|
1f0574403c | ||
|
|
fa0a47d18f | ||
|
|
5af18d46f0 | ||
|
|
ac98a7ad72 | ||
|
|
06095f60c1 | ||
|
|
d60372e70c | ||
|
|
e533c6126d | ||
|
|
017edee786 | ||
|
|
fe308118aa | ||
|
|
8426e99d64 | ||
|
|
91f3a7edc1 | ||
|
|
58299fa16c | ||
|
|
177add6939 | ||
|
|
f68f400832 | ||
|
|
c92f36e531 | ||
|
|
7fe54e2450 | ||
|
|
187872a618 | ||
|
|
e2b8f6f0c5 | ||
|
|
124b387b41 | ||
|
|
e9b1a3d9a7 | ||
|
|
428e38d55a | ||
|
|
3124511a43 | ||
|
|
a0594369c9 | ||
|
|
7130b11e31 | ||
|
|
6922f4418b | ||
|
|
4851418373 | ||
|
|
943016ff64 | ||
|
|
82d09efbbe | ||
|
|
29d8e2a63b | ||
|
|
ca59de20b6 | ||
|
|
d26a6f62ca | ||
|
|
2ea6043e99 | ||
|
|
8cd68cb442 | ||
|
|
04bf0a0a0d | ||
|
|
2e5675038d | ||
|
|
1f87fc6082 | ||
|
|
9200659b20 | ||
|
|
525988018f | ||
|
|
45e9b13f41 | ||
|
|
990c352308 | ||
|
|
353d466389 | ||
|
|
3f7b5f5359 | ||
|
|
a17336e730 | ||
|
|
cb6b967a15 | ||
|
|
550b60b3c0 | ||
|
|
09d613952d | ||
|
|
062495893a | ||
|
|
a56a5be59f | ||
|
|
eb11374a85 | ||
|
|
fa1f81724f | ||
|
|
fd25899099 | ||
|
|
2c46df5690 | ||
|
|
3495fa5a9f | ||
|
|
cd0cffbc69 | ||
|
|
3385e431f9 | ||
|
|
809d4d8e72 | ||
|
|
d03e3ccba3 | ||
|
|
982adbe55b | ||
|
|
3cc0265ca8 | ||
|
|
9a751539b8 | ||
|
|
1c90fe0aa0 | ||
|
|
c13bdaa805 | ||
|
|
fb18213539 | ||
|
|
f11d40e07b | ||
|
|
1cd6764bb6 | ||
|
|
f7fb8aba16 | ||
|
|
e886a69ed0 | ||
|
|
145080334f | ||
|
|
bfa9efbbd5 | ||
|
|
672348eab6 | ||
|
|
d36eca9ccd | ||
|
|
461bcb5521 | ||
|
|
4e2a2be702 | ||
|
|
db453cd291 | ||
|
|
100fbf4140 | ||
|
|
6dfdd01378 | ||
|
|
24a24a73fb | ||
|
|
2852eb645f | ||
|
|
611696bd4c | ||
|
|
49ac99c19f | ||
|
|
d5edfb7049 | ||
|
|
3e34c35849 | ||
|
|
39ff83ad97 | ||
|
|
ed36ecd116 | ||
|
|
12aec3d790 | ||
|
|
b9b1114704 | ||
|
|
1c638f046e | ||
|
|
18907eb20d | ||
|
|
ba3559c81a | ||
|
|
02ae598c08 | ||
|
|
a2668acbf1 | ||
|
|
e400ea9394 | ||
|
|
2f7659bb53 | ||
|
|
a7712a4613 | ||
|
|
821774205a | ||
|
|
085d822377 | ||
|
|
195683e8a7 | ||
|
|
6de31a94c2 | ||
|
|
9d3b9208c8 | ||
|
|
8cae3e2279 | ||
|
|
075472af6c | ||
|
|
6d202bd8b9 | ||
|
|
0dbbffce6d | ||
|
|
eac36abc1f | ||
|
|
96a2da5971 | ||
|
|
5cef1c66cb | ||
|
|
395e71d619 | ||
|
|
6a8499e484 | ||
|
|
0c75c442dd | ||
|
|
5ef2768e27 | ||
|
|
17170967ee | ||
|
|
0fa1d0c7ac | ||
|
|
2053463f0e | ||
|
|
a91a871822 | ||
|
|
ca5a635833 | ||
|
|
1f0f246359 | ||
|
|
49b6cd705d | ||
|
|
0f00db8568 | ||
|
|
8f97826ff0 | ||
|
|
b7d3a8c120 | ||
|
|
2a66cf4110 | ||
|
|
6cc2e89aa7 | ||
|
|
6ec577ba90 | ||
|
|
a8bc1ee3f7 | ||
|
|
3903841de2 | ||
|
|
9d9ae8e22a | ||
|
|
cc243303fa | ||
|
|
ac2e6fd69b | ||
|
|
83c586505d | ||
|
|
f0630df178 | ||
|
|
3f5697cafa | ||
|
|
29c1eecc7c | ||
|
|
aeaa8557ea | ||
|
|
b2930b6c8b | ||
|
|
a44a67a48b | ||
|
|
bcd2496a57 | ||
|
|
1a75e298e4 | ||
|
|
aa91a75c78 | ||
|
|
dbd98e2e21 | ||
|
|
d3e09687f6 | ||
|
|
8ce6ca0479 | ||
|
|
458801887e | ||
|
|
0b26c372c4 | ||
|
|
f232cefe60 | ||
|
|
b823f4f84a | ||
|
|
44f8b33912 | ||
|
|
dbc6c42a20 | ||
|
|
ce7d99fa4f | ||
|
|
2fd47ec212 | ||
|
|
d12e02f889 | ||
|
|
fb8a88841b | ||
|
|
4db5f64d40 | ||
|
|
e1308e4000 | ||
|
|
e5e9cf7ae1 | ||
|
|
b61bb4b147 | ||
|
|
32e87159ad | ||
|
|
1ac120fb8d | ||
|
|
820aaadca1 | ||
|
|
f7e48c5e5e | ||
|
|
3615d64916 | ||
|
|
fc53c3e13b | ||
|
|
4687d9edc6 | ||
|
|
9e3dbd6bc5 | ||
|
|
64a4c7fcd7 | ||
|
|
4945f457c3 | ||
|
|
cc57f0ae50 | ||
|
|
6a3ee98f84 | ||
|
|
1192fd2e54 | ||
|
|
a4ef587756 | ||
|
|
378009249a | ||
|
|
e8f15a2c4b | ||
|
|
4db2d80a2c | ||
|
|
05f1e81db2 | ||
|
|
22d75b477c | ||
|
|
e3b2785413 | ||
|
|
d1f9ccecaa | ||
|
|
9082473005 | ||
|
|
03730e3d19 | ||
|
|
31b5b6d13e | ||
|
|
441d6c5692 | ||
|
|
e28c798329 | ||
|
|
05eb83e167 | ||
|
|
1015cf5024 | ||
|
|
39f729ed5f | ||
|
|
36437b7705 | ||
|
|
6853180936 | ||
|
|
ac41bf243d | ||
|
|
03649afa9d | ||
|
|
c8b3d98ee7 | ||
|
|
0b5bae214e | ||
|
|
93d7b59a6b | ||
|
|
71494d0819 | ||
|
|
09b6fc99ad | ||
|
|
d920f02ab2 | ||
|
|
4f8a396f3f | ||
|
|
d44556e889 | ||
|
|
0a7f1882ce | ||
|
|
11dc9136ca | ||
|
|
16c9459b51 | ||
|
|
a34f3fd5ac | ||
|
|
072bc67352 | ||
|
|
ef2a11a594 | ||
|
|
52f0cdaad3 | ||
|
|
4d82f05845 | ||
|
|
86f2a14a1a | ||
|
|
393dc20d65 | ||
|
|
413fabf3cd | ||
|
|
afbf10710e | ||
|
|
2a6bdff636 | ||
|
|
07a7602383 | ||
|
|
d62ace8ddf | ||
|
|
70032cedc5 | ||
|
|
43d5e7ff99 | ||
|
|
47e0ec3961 | ||
|
|
64f1d02a70 | ||
|
|
4318eea29d | ||
|
|
3a9c197f8f | ||
|
|
4ae325cae8 | ||
|
|
1380d6fdc8 | ||
|
|
da5b134efb | ||
|
|
83235c5cfc | ||
|
|
2da42f54cf | ||
|
|
a0cf97c85b | ||
|
|
9328b9bc8c | ||
|
|
815caffbdd | ||
|
|
32d40e71a4 | ||
|
|
98861b9961 | ||
|
|
3995f7117e | ||
|
|
2e5fbbd83b | ||
|
|
2dac5cb5f5 | ||
|
|
46ac63fd6e | ||
|
|
7c7cd4ae78 | ||
|
|
f41793fd86 | ||
|
|
c4d23753cc | ||
|
|
ba9b9d1a8d | ||
|
|
2b6b9ad795 | ||
|
|
8ed36b0a9e | ||
|
|
bba094fb3c | ||
|
|
c8e181506e | ||
|
|
1b18140cc4 | ||
|
|
2aa0a0229c | ||
|
|
00c98a59c9 | ||
|
|
c1c45e8005 | ||
|
|
216fdb34d0 | ||
|
|
5bb30f7b96 | ||
|
|
bf05b5492f | ||
|
|
2a76b9cd24 | ||
|
|
aa964191da | ||
|
|
9b5d772b24 | ||
|
|
8a03da0caf | ||
|
|
1dbad6aa20 | ||
|
|
a3894a1aa7 | ||
|
|
253fac8b0e | ||
|
|
f30242106d | ||
|
|
cddfdcba08 | ||
|
|
2c0a215bcd | ||
|
|
066ab9641c | ||
|
|
b1c884bb38 | ||
|
|
bc099d8579 | ||
|
|
f87f717bd5 | ||
|
|
3cf8c895ed | ||
|
|
fca7a3d242 | ||
|
|
26a0d5124e | ||
|
|
9b6c7676f6 | ||
|
|
cf71677276 | ||
|
|
da2acd59ca | ||
|
|
07e5ebcf19 | ||
|
|
6c41b7d88d | ||
|
|
341140aea6 | ||
|
|
47f0e31a3e | ||
|
|
10dd9b2785 | ||
|
|
a5878f96ac | ||
|
|
4c90f9b5e0 | ||
|
|
f581a50d68 | ||
|
|
d98c44f4b0 | ||
|
|
3ac2a593be | ||
|
|
2ccd504245 | ||
|
|
116825a97a | ||
|
|
19ba6ea2d6 | ||
|
|
397e9924de | ||
|
|
0411380f89 | ||
|
|
cb2a3286fc | ||
|
|
e6b5835717 | ||
|
|
1079d3b695 | ||
|
|
3c75de82f7 | ||
|
|
f5dce5fdbf | ||
|
|
9b0ff8e138 | ||
|
|
2f68bc51f7 | ||
|
|
2601de3535 | ||
|
|
dad7788e5c | ||
|
|
148e3021b1 | ||
|
|
bddb09fc2c | ||
|
|
3d096ffb0e | ||
|
|
96dcdf7643 | ||
|
|
d6c193e1cc | ||
|
|
24cfe10ffe | ||
|
|
359d11f35a | ||
|
|
7a68833f86 | ||
|
|
b8202e38d3 | ||
|
|
d51fd7f600 | ||
|
|
661b745962 | ||
|
|
aa0bbaea56 | ||
|
|
c42f0d5a56 | ||
|
|
b7822a1753 | ||
|
|
4509f4b6d2 | ||
|
|
15cb528c02 | ||
|
|
7f39c67eab | ||
|
|
83fc231b3d | ||
|
|
5cbf3982d0 | ||
|
|
a741b8a8d2 | ||
|
|
a93ca81d4c | ||
|
|
b47aee0bce | ||
|
|
dbd84945e3 | ||
|
|
c92d2d2224 | ||
|
|
a698b030d9 | ||
|
|
ff99b45b20 | ||
|
|
88391d0a1f | ||
|
|
585943d912 | ||
|
|
d3fddf862f | ||
|
|
4a171b628c | ||
|
|
729b9b3221 | ||
|
|
7845091a5c | ||
|
|
ba7589d733 | ||
|
|
4ad8072070 | ||
|
|
aff9f22d77 | ||
|
|
ddc36e93a2 | ||
|
|
72e4ad49df | ||
|
|
5e07790ffa | ||
|
|
45b0951020 | ||
|
|
88e83067ed | ||
|
|
ff03c32842 | ||
|
|
cd2670b99e | ||
|
|
56953edd04 | ||
|
|
087359e170 | ||
|
|
c0f2f96c8e | ||
|
|
439d97a2a4 | ||
|
|
71613f02d2 | ||
|
|
d6d7d951ee | ||
|
|
defedf5157 | ||
|
|
448bb2ee71 | ||
|
|
4a0c750273 | ||
|
|
1437931f4f | ||
|
|
13e1965450 | ||
|
|
eb4ed35c88 | ||
|
|
3d91d86402 | ||
|
|
63b5a7fe88 | ||
|
|
432c11a5c4 | ||
|
|
1c53f1113d | ||
|
|
0f0eb33fff | ||
|
|
e2ad1b7ccb | ||
|
|
8d27247919 | ||
|
|
bbc7731258 | ||
|
|
f2df7dc9ee | ||
|
|
efd80f72da | ||
|
|
de20f45917 | ||
|
|
744c590139 | ||
|
|
078c8158ab | ||
|
|
802480b9a4 | ||
|
|
d577b1084d | ||
|
|
4f2a999a49 | ||
|
|
f8354b087e | ||
|
|
3ec4ef0f4b | ||
|
|
91f859e9e6 | ||
|
|
8edba786c6 | ||
|
|
fe21b49bac | ||
|
|
7d25fb40c5 | ||
|
|
04d34e7380 | ||
|
|
a17127f0ee | ||
|
|
db27099133 | ||
|
|
1deaaf0def | ||
|
|
a00ae01470 | ||
|
|
69721a9588 | ||
|
|
6b989c22fc | ||
|
|
1f077e0f15 | ||
|
|
cfcf8ba09f | ||
|
|
b200e31945 | ||
|
|
f0b8f16a50 | ||
|
|
7adff98a59 | ||
|
|
686b9f84a3 | ||
|
|
cf13970f7b | ||
|
|
9f32228e21 | ||
|
|
27a6a5c1aa | ||
|
|
b22d25a177 | ||
|
|
34a1e4fdea | ||
|
|
552e5b754b | ||
|
|
a8034d155f | ||
|
|
313b3f2f64 | ||
|
|
70bc27626b | ||
|
|
ae25e8bcee | ||
|
|
bb268e4a1d | ||
|
|
9f14bb1cf1 | ||
|
|
628f29a275 | ||
|
|
3c6c4b8784 | ||
|
|
3917e0433d | ||
|
|
4e2c33b35d | ||
|
|
9a9395fdc6 | ||
|
|
34ead39640 | ||
|
|
61f34ef3e4 | ||
|
|
895be9e8dd | ||
|
|
b56a4efc90 | ||
|
|
e3e78985d0 | ||
|
|
567aa9978c | ||
|
|
17f8baf0db | ||
|
|
a2c6331d35 | ||
|
|
5c0f9883f1 | ||
|
|
f1667843e8 | ||
|
|
a6078ea0ec | ||
|
|
7244c0adc6 | ||
|
|
1da38a783a | ||
|
|
6c5feaf342 | ||
|
|
20c1016b27 | ||
|
|
82a72d563e | ||
|
|
c73d03ac61 | ||
|
|
cd5ae52b9f | ||
|
|
d696193065 | ||
|
|
09c766c945 | ||
|
|
42e7f27e2a | ||
|
|
e875937aa9 | ||
|
|
4f51993bda | ||
|
|
d4488c4beb | ||
|
|
572cf6fa43 | ||
|
|
3965765ded | ||
|
|
8b88a71c9d | ||
|
|
cac8c943b2 | ||
|
|
81333e4192 | ||
|
|
18e11b9f8c | ||
|
|
6ef9730420 | ||
|
|
4e63287d3b | ||
|
|
2199d84a46 | ||
|
|
b03b13ff99 | ||
|
|
68a9c2f38d | ||
|
|
4273776365 | ||
|
|
1372bbc5f4 | ||
|
|
3d71691437 | ||
|
|
2621306f17 | ||
|
|
c394a14c05 | ||
|
|
fd3d6e8bcb | ||
|
|
c6c1e64300 | ||
|
|
0ad3e3c209 | ||
|
|
9548d88259 | ||
|
|
94fd9494e5 | ||
|
|
47a3e2b393 | ||
|
|
b59c2c6216 | ||
|
|
52c7470c79 | ||
|
|
b006972bcb | ||
|
|
5a6d3b220f | ||
|
|
8123e8db9d | ||
|
|
93d067a41d | ||
|
|
38c365d326 | ||
|
|
16c34eee46 | ||
|
|
233825016c | ||
|
|
62a7195702 | ||
|
|
9ccb814279 | ||
|
|
3020ec1ff8 | ||
|
|
696ea5c229 | ||
|
|
b5563c98c1 | ||
|
|
f6c59bdec5 | ||
|
|
ab16865dcc | ||
|
|
487f794ef1 | ||
|
|
46a95a0153 | ||
|
|
dff2c2e343 | ||
|
|
b4f1a49432 | ||
|
|
f178ef76b8 | ||
|
|
ad2f064ed9 | ||
|
|
2f0aa2bb51 | ||
|
|
30dc54d08a | ||
|
|
e00177c0b9 | ||
|
|
caeacd265d | ||
|
|
84533fe8c4 | ||
|
|
5f970a7a2e | ||
|
|
63c6e1381e | ||
|
|
ed455a4bbe | ||
|
|
4760c42417 | ||
|
|
4615df9592 | ||
|
|
45ac52ee2c | ||
|
|
4626f5f363 | ||
|
|
d42f558e6a | ||
|
|
c35edcd6ce | ||
|
|
5304b482cc | ||
|
|
7d1b7f557c | ||
|
|
a6eabc61e5 | ||
|
|
a481a39104 | ||
|
|
7bff93dca6 | ||
|
|
625e389920 | ||
|
|
cd54603b82 | ||
|
|
eacb71a010 | ||
|
|
05860be267 | ||
|
|
e5f9452bfc | ||
|
|
0c17a50ac6 | ||
|
|
89798b2196 | ||
|
|
a01e66dfd6 | ||
|
|
3e2d827436 | ||
|
|
0a745011fd | ||
|
|
33c885dc65 | ||
|
|
2a96d55652 | ||
|
|
4f97729bd5 | ||
|
|
95fad6ca72 | ||
|
|
7627da9b18 | ||
|
|
c2d9e08711 | ||
|
|
3183197cf7 | ||
|
|
82bcd18416 | ||
|
|
ee88746fdf | ||
|
|
6a1ffc44ea | ||
|
|
210e9c9e91 | ||
|
|
1e0b0818d9 | ||
|
|
f3661382e4 | ||
|
|
022f0648d9 | ||
|
|
3186999a63 | ||
|
|
047a3c2d12 | ||
|
|
9f8b6668bb | ||
|
|
a4b0c07183 | ||
|
|
176f616461 | ||
|
|
a5c9512452 | ||
|
|
2542cbadf0 | ||
|
|
a91a8da595 | ||
|
|
82f70b0b92 | ||
|
|
3dfc618742 | ||
|
|
049b9ba967 | ||
|
|
261833b571 | ||
|
|
d5aa78d5af | ||
|
|
0744b2c0ed | ||
|
|
954e6b926a | ||
|
|
9f9981f1eb | ||
|
|
a8abbe9a2a | ||
|
|
63fc72e014 | ||
|
|
d48685876a | ||
|
|
d699eaec90 | ||
|
|
2755b86506 | ||
|
|
b93447d5be | ||
|
|
efb2087dcf | ||
|
|
f33dab7054 | ||
|
|
7096d98d6b | ||
|
|
d113aac209 | ||
|
|
8d5700ef86 | ||
|
|
fd4acad0a0 | ||
|
|
ece84808fb | ||
|
|
0d3b182f51 | ||
|
|
0b676d723a | ||
|
|
a1449aa3b9 | ||
|
|
ea9371c87f | ||
|
|
196fdc398e | ||
|
|
3ecf628240 | ||
|
|
fee6ffe041 | ||
|
|
8801d3ac82 | ||
|
|
d0a6386e7a | ||
|
|
a77fbf7f48 | ||
|
|
879cf8bb65 | ||
|
|
3ae6f7f0d4 | ||
|
|
5b7d0e599b | ||
|
|
9b0bb61f70 | ||
|
|
a4578662d8 | ||
|
|
3b0d53dede | ||
|
|
9e62a3e303 | ||
|
|
cae6e8fbce | ||
|
|
7a4c055861 | ||
|
|
ec80f75ff1 | ||
|
|
aea4321cfc | ||
|
|
249ece843f | ||
|
|
1a51d26481 | ||
|
|
c903c9f96c | ||
|
|
c8a81dd75f | ||
|
|
837e69bb2a | ||
|
|
ceeb4095c1 | ||
|
|
f2e2394169 | ||
|
|
ad30b78682 | ||
|
|
5452118bc4 | ||
|
|
b4d947f7fa | ||
|
|
03a5557988 | ||
|
|
bf9799677b | ||
|
|
5fd9397cad | ||
|
|
be18eda567 | ||
|
|
6192aadefd | ||
|
|
c1730405fb | ||
|
|
dda979f9c3 | ||
|
|
3deca63ce2 | ||
|
|
bda02c514b | ||
|
|
adbae13d88 | ||
|
|
deeb7efa6f | ||
|
|
1a1535d8f5 | ||
|
|
b7a09055bf | ||
|
|
aa6dd269f2 | ||
|
|
c374cef861 | ||
|
|
dc9d641464 | ||
|
|
f52a35447d | ||
|
|
f7a65f799a | ||
|
|
150e01a2ce | ||
|
|
47a945fa11 | ||
|
|
f495ee23ce | ||
|
|
13526a6383 | ||
|
|
e784389c07 | ||
|
|
7290c18cbb | ||
|
|
7c3803c736 | ||
|
|
2b90ed6378 | ||
|
|
b02b6e1e61 | ||
|
|
289d2e3793 | ||
|
|
9d5c418113 | ||
|
|
b45647a568 | ||
|
|
32a1cacf74 | ||
|
|
57a0876bc3 | ||
|
|
f75ef7f10b | ||
|
|
6ba3b913ef | ||
|
|
705273060d | ||
|
|
39a844ea35 | ||
|
|
52c84d742c | ||
|
|
73a2cffca5 | ||
|
|
e86120d50c | ||
|
|
118a4b1433 | ||
|
|
2cbfd8eb13 | ||
|
|
6fc230ccdb | ||
|
|
c17a6c161d | ||
|
|
593bbc1064 | ||
|
|
a71fc3bcc0 | ||
|
|
f500554174 | ||
|
|
207c7c03e0 | ||
|
|
90f0e1a918 | ||
|
|
e764762787 | ||
|
|
cfb2e37f72 | ||
|
|
54fd1bd674 | ||
|
|
9ba5b95643 | ||
|
|
9ce93e54d0 | ||
|
|
18890d90bf | ||
|
|
f03012e351 | ||
|
|
ef84cacec4 | ||
|
|
cfa2f8be6b | ||
|
|
bbc09f5585 | ||
|
|
28e8a1fe7b | ||
|
|
41b7600f20 | ||
|
|
6fe2872181 | ||
|
|
3d4b19f183 | ||
|
|
3c284f712b | ||
|
|
babe371135 | ||
|
|
bcb389bf03 | ||
|
|
68775257fe | ||
|
|
d618e64147 | ||
|
|
4042b58393 | ||
|
|
ba74b6777c | ||
|
|
aff1adf9c2 | ||
|
|
d3e9b99aa9 | ||
|
|
7fca859c6f | ||
|
|
1520583713 | ||
|
|
fc80c241f0 | ||
|
|
3c2fbd6f66 | ||
|
|
6be4cec571 | ||
|
|
02eecbf15a | ||
|
|
113f2c1d3e | ||
|
|
1f40a7ab0b | ||
|
|
6123c580ab | ||
|
|
d0b49404b3 | ||
|
|
c57590cd93 | ||
|
|
7ee15b7356 | ||
|
|
044aa57a3d | ||
|
|
6166958291 | ||
|
|
5fa5efe50a | ||
|
|
e1581af64b | ||
|
|
692a337a84 | ||
|
|
0a251a4684 | ||
|
|
d864b4656b | ||
|
|
1a2c236975 | ||
|
|
4a1c65786c | ||
|
|
56b15007d4 | ||
|
|
d00c1affda | ||
|
|
c8d3add47f | ||
|
|
5dae9980d4 | ||
|
|
ad3053a8da | ||
|
|
5a11996e00 | ||
|
|
89119d76ea | ||
|
|
350be5f4d0 | ||
|
|
357004c3df | ||
|
|
f4737b1be4 | ||
|
|
aa737eabd4 | ||
|
|
793d9d41f7 | ||
|
|
7741a0cc2a | ||
|
|
b09ccc9cea | ||
|
|
14c3152c57 | ||
|
|
e189614bf6 | ||
|
|
4c38c46eeb | ||
|
|
d37eae4802 | ||
|
|
a1c9816e19 | ||
|
|
b0cbbb7b76 | ||
|
|
ffc3a175d3 | ||
|
|
78f45d8cd6 | ||
|
|
d403c9e209 | ||
|
|
e1a656547a | ||
|
|
3f2b3ea678 | ||
|
|
cb3ff53bed | ||
|
|
6b616487ed | ||
|
|
1f044f5204 | ||
|
|
b4dffcc01f | ||
|
|
954330edef | ||
|
|
d648aff14f | ||
|
|
b06239005f | ||
|
|
0ddb06893f | ||
|
|
93b0ca6500 | ||
|
|
143f697466 | ||
|
|
17d5df8c13 | ||
|
|
0b151a6467 | ||
|
|
5aa9bf9035 | ||
|
|
7ff0382d42 | ||
|
|
6ea280e7b4 | ||
|
|
b571122e7f | ||
|
|
621386f6d5 | ||
|
|
d2673e99f7 | ||
|
|
ea8fd7b1e8 | ||
|
|
f7d9ae01b8 | ||
|
|
e2ca17d916 | ||
|
|
cbba540981 | ||
|
|
c8d68eb515 | ||
|
|
dff05919a7 | ||
|
|
aeed5fab9f | ||
|
|
d6a2821622 | ||
|
|
eef720006e | ||
|
|
99f5a36061 | ||
|
|
bcc3e3bf2a | ||
|
|
5df6da5704 | ||
|
|
dbc1fc119c | ||
|
|
e5c9d750b1 | ||
|
|
c4bf1a2dae | ||
|
|
400abe628c | ||
|
|
8de3dce014 | ||
|
|
a0513a041a | ||
|
|
5cda0bfb6c | ||
|
|
f64c498729 | ||
|
|
7cd02d6b8c | ||
|
|
c265c29fd1 | ||
|
|
453667b87b | ||
|
|
7c42af7b35 | ||
|
|
dcc58c32ba | ||
|
|
9de26bd4d1 | ||
|
|
0c2d9b9df5 | ||
|
|
6045d7b5e8 | ||
|
|
6486f4dd9b | ||
|
|
b4b63124b9 | ||
|
|
f887d66b3a | ||
|
|
dbb56b047d | ||
|
|
2b29a00256 | ||
|
|
01e5f60eec | ||
|
|
ff5d993b43 | ||
|
|
ae02a4236a | ||
|
|
eb2de50030 | ||
|
|
17bc438b77 | ||
|
|
d9d681ed98 | ||
|
|
df9d71ccf8 | ||
|
|
d975ce905c | ||
|
|
f0502a0bfb | ||
|
|
240612f9a3 | ||
|
|
884efe0ad9 | ||
|
|
c55703a988 | ||
|
|
6486b480ff | ||
|
|
7768cc8550 | ||
|
|
a21be5add8 | ||
|
|
cbf5b83126 | ||
|
|
0b463856d3 | ||
|
|
ddcca11aff | ||
|
|
28c41fdc0a | ||
|
|
31ef4efa06 | ||
|
|
1b3e551428 | ||
|
|
82676b4cbb | ||
|
|
7a767314b7 | ||
|
|
900af42e10 | ||
|
|
f6a5e4a232 | ||
|
|
0cfdfe02e6 | ||
|
|
7f8a99c75e | ||
|
|
9d2cb34c9b | ||
|
|
8e09691846 | ||
|
|
4024145a3c | ||
|
|
ab0829a72f | ||
|
|
effb7b41f6 | ||
|
|
b7b774bc4e | ||
|
|
188bd9908f | ||
|
|
ec79afe9c0 | ||
|
|
ba0237d36b | ||
|
|
e033b263a5 | ||
|
|
875724a9f7 | ||
|
|
a5ed0de28f | ||
|
|
77499f688f | ||
|
|
38ffceef2e | ||
|
|
df3e9dd4ec | ||
|
|
796b50b92c | ||
|
|
eff89ed3ac | ||
|
|
0dbeff7bd5 | ||
|
|
b7b4957fd9 | ||
|
|
f95790603c | ||
|
|
c3004ac300 | ||
|
|
25f1d4d28c | ||
|
|
0c5bb6f1ef | ||
|
|
5aff82931b | ||
|
|
c2eed20dd2 | ||
|
|
8df2a2b3d3 | ||
|
|
f405d46271 | ||
|
|
2a2d4add0b | ||
|
|
6b425161fe | ||
|
|
bb71323c4d | ||
|
|
6759a62ac6 | ||
|
|
32ca74623a | ||
|
|
860918adfe | ||
|
|
4c9e4b12d1 | ||
|
|
a26f143ad3 | ||
|
|
cd8bc03a29 | ||
|
|
6c8b1d6345 | ||
|
|
61b1025105 | ||
|
|
037208cc8c | ||
|
|
0d7b843d5d | ||
|
|
b1810e40c7 | ||
|
|
314603c64c | ||
|
|
e0dfd4d76c | ||
|
|
b5470871b8 | ||
|
|
9f2fce51ed | ||
|
|
63a23a7706 | ||
|
|
8289b7537b | ||
|
|
64a6e1a0d8 | ||
|
|
ccf16bc392 | ||
|
|
09481a46d1 | ||
|
|
9e59a0d5ef | ||
|
|
5cb81c7d58 | ||
|
|
325bea9cda | ||
|
|
382434b1dc | ||
|
|
b21b682d4b | ||
|
|
c504e3ed4f | ||
|
|
e9c4beac08 | ||
|
|
1944a21f4d | ||
|
|
702943d450 | ||
|
|
b2e082c35f | ||
|
|
d7e72ee668 | ||
|
|
88299939c5 | ||
|
|
843f1b9996 | ||
|
|
e7b1180e8a | ||
|
|
ccdbcc68f3 | ||
|
|
27054df1e7 | ||
|
|
9f6bc50ee6 | ||
|
|
3904a6961d | ||
|
|
b0e9c89d69 | ||
|
|
536b6017e4 | ||
|
|
5858a014a2 | ||
|
|
64503f2eff | ||
|
|
6b86c37621 | ||
|
|
1058b8ac92 | ||
|
|
e1644b9fd8 | ||
|
|
83353aa7a3 | ||
|
|
74fbb29bfa | ||
|
|
5b2a22b9d4 | ||
|
|
ba1b79ef76 | ||
|
|
f54ba04fdf | ||
|
|
0e8bc6d3a0 | ||
|
|
e0bfa02f6a | ||
|
|
d855a36476 | ||
|
|
8a9458b558 | ||
|
|
d88a36a2d8 | ||
|
|
93bc46c34d | ||
|
|
fbb41c3665 | ||
|
|
85d1f62398 | ||
|
|
202e9f0f0a | ||
|
|
7fcb212b6c | ||
|
|
80ea537c50 | ||
|
|
334d5d238d | ||
|
|
eee535baef | ||
|
|
44c49064a5 | ||
|
|
ca9d8c4613 | ||
|
|
c475944c29 | ||
|
|
3540892045 | ||
|
|
ccdafcdae7 | ||
|
|
ddf4b5a38e | ||
|
|
e74666407e | ||
|
|
8cba67130f | ||
|
|
953f32bdfc | ||
|
|
8be9edfc9e | ||
|
|
24c5864d6d | ||
|
|
78fec7dc58 | ||
|
|
11039c06b8 | ||
|
|
17effd594f | ||
|
|
447acb3bf3 | ||
|
|
49f9cb6800 | ||
|
|
2f764c4876 | ||
|
|
7959c30405 | ||
|
|
26245ef789 | ||
|
|
2032a29f75 | ||
|
|
88fcd434c4 | ||
|
|
f8b58388d6 | ||
|
|
444182faec | ||
|
|
4eae3eb43d | ||
|
|
b6ff85dffa | ||
|
|
bd44661b8a | ||
|
|
adc687767b | ||
|
|
4af11648ee | ||
|
|
d73d5e12fd | ||
|
|
b70bb2e210 | ||
|
|
26b7a1b993 | ||
|
|
adbc203cac | ||
|
|
e61f3bb268 | ||
|
|
0855fea703 | ||
|
|
b5bb909605 | ||
|
|
5f6f5ecc5a | ||
|
|
949e36833c | ||
|
|
0e5d690d4b | ||
|
|
0a96c0f0ac | ||
|
|
a747c9afea | ||
|
|
9bdc863d01 | ||
|
|
c059f7ef3d | ||
|
|
9cd4007532 | ||
|
|
51ee1d2a57 | ||
|
|
1172e84d75 | ||
|
|
346bff9e46 | ||
|
|
9243fc606f | ||
|
|
3f71ccf74d | ||
|
|
ebf51eab16 | ||
|
|
193e69018c | ||
|
|
6016d987e0 | ||
|
|
c4238774a1 | ||
|
|
0d79dc0b52 | ||
|
|
338f2ca719 | ||
|
|
3687382a65 | ||
|
|
45ef796c26 | ||
|
|
f8325dc4bb | ||
|
|
9bf13d5e58 | ||
|
|
245d34fbc0 | ||
|
|
657a1d07c5 | ||
|
|
ca8da85868 | ||
|
|
c49c0fa43c | ||
|
|
75443bbeb5 | ||
|
|
e89013b9ad | ||
|
|
1581c6c81c | ||
|
|
9985c029ca | ||
|
|
32132fb3d1 | ||
|
|
5e8ab73821 | ||
|
|
dba9ee3541 | ||
|
|
6bd669953f | ||
|
|
65261995bd | ||
|
|
89285894b2 | ||
|
|
f156b70ad9 | ||
|
|
f03e121f58 | ||
|
|
ea67d5caec | ||
|
|
a28ce07c74 | ||
|
|
00711dd018 | ||
|
|
273dd27c01 | ||
|
|
5739bf6aa2 | ||
|
|
073a3be8f5 | ||
|
|
183d0862c1 | ||
|
|
5ffd851350 | ||
|
|
57c4a9baa1 | ||
|
|
6277de5b3f | ||
|
|
be3f85a445 | ||
|
|
03b9c88823 | ||
|
|
92b9a00d8c | ||
|
|
208de7f9ab | ||
|
|
b21951b632 | ||
|
|
ca986c0fa7 | ||
|
|
7f7b52b9ec | ||
|
|
2eedb8e905 | ||
|
|
676f0f4773 | ||
|
|
41fa9e43de | ||
|
|
4a7861827d | ||
|
|
16c3bcf551 | ||
|
|
6458ca90dc | ||
|
|
2d964873ff | ||
|
|
d7ab3f7818 | ||
|
|
2f8e3661d6 | ||
|
|
e9c7b432df | ||
|
|
0a9c303769 | ||
|
|
f64e30f319 | ||
|
|
23bc24ac68 | ||
|
|
04b2671a96 | ||
|
|
4ba4a6dbcd | ||
|
|
e21d67936c | ||
|
|
fd2095efde | ||
|
|
07b89a7806 | ||
|
|
97ba3fa280 | ||
|
|
1b6f1b57c0 | ||
|
|
24da58c4b8 | ||
|
|
adee55be54 | ||
|
|
2298e37201 | ||
|
|
a4e2963832 | ||
|
|
b63808f316 | ||
|
|
4f6b5cf683 | ||
|
|
4aad504ea8 | ||
|
|
c8abd12da0 | ||
|
|
fdd41f0244 | ||
|
|
24f43febc8 | ||
|
|
893e047005 | ||
|
|
8024701184 | ||
|
|
b556d2e563 | ||
|
|
61a877c95f | ||
|
|
e8fd8b26e3 | ||
|
|
8eb1131d14 | ||
|
|
48e55a0d5f | ||
|
|
da1726ac08 | ||
|
|
9a72abb672 | ||
|
|
8d43883d1e | ||
|
|
db98bc618e | ||
|
|
a7b76b6524 | ||
|
|
520f303374 | ||
|
|
70b5ac1d6b | ||
|
|
c6384dc8b1 | ||
|
|
f28050a5f5 | ||
|
|
774f974a71 | ||
|
|
40f913217c | ||
|
|
7fa215f38f | ||
|
|
4bfa99541f | ||
|
|
c485413b65 | ||
|
|
6cbb6c531b | ||
|
|
65694a7c39 | ||
|
|
57bb51be2a | ||
|
|
61579f28f7 | ||
|
|
195101795c | ||
|
|
0d8976ae8a | ||
|
|
5144f859ee | ||
|
|
9d2c4a23e0 | ||
|
|
a56432816d | ||
|
|
d4b14d2800 | ||
|
|
6362213063 | ||
|
|
d791e54421 | ||
|
|
7864b7b86d | ||
|
|
4d42ef00ac | ||
|
|
1df1a8cc54 | ||
|
|
952691f572 | ||
|
|
9ef469dd4d | ||
|
|
cb6727d181 | ||
|
|
5870a6792e | ||
|
|
569a188c9a | ||
|
|
bb513f6b27 | ||
|
|
6ef70eeea0 | ||
|
|
d7959041c8 | ||
|
|
3305f93e32 | ||
|
|
cfca0ac1da | ||
|
|
075c7f7e41 | ||
|
|
10e0d20a99 | ||
|
|
b7df9d6bb4 | ||
|
|
b261385e63 | ||
|
|
cadbd66684 | ||
|
|
9e94a01ec0 | ||
|
|
2be7c520d7 | ||
|
|
843ae657b9 | ||
|
|
6200028366 | ||
|
|
a7bb544e5c | ||
|
|
0b427bb80b | ||
|
|
06ff400704 | ||
|
|
922627a397 | ||
|
|
5cb17992e8 | ||
|
|
36a6489db0 | ||
|
|
3a6a87c71e | ||
|
|
c1aa724fa7 | ||
|
|
97125aa846 | ||
|
|
f4d07e15ea | ||
|
|
59a4212cac | ||
|
|
d1dcf88741 | ||
|
|
66472ce192 | ||
|
|
b6f69ecfff | ||
|
|
c73e1972af | ||
|
|
84dda3d130 | ||
|
|
6a23056cfc | ||
|
|
601ecff43c | ||
|
|
395c0a5fcd | ||
|
|
d6e6f5f8ab | ||
|
|
63b88d2393 | ||
|
|
0a85b90f7c | ||
|
|
47e7ecbd81 | ||
|
|
64ac21d8c2 | ||
|
|
bb1fb40112 | ||
|
|
17a0c24845 | ||
|
|
305a62fa09 | ||
|
|
c7032f7557 | ||
|
|
656ce3001b | ||
|
|
77e9404f19 | ||
|
|
91b4cc8fa8 | ||
|
|
ec065cdeee | ||
|
|
a4ecce3425 | ||
|
|
7a9e006d6a | ||
|
|
7e53401a41 | ||
|
|
1a71153231 | ||
|
|
4eab2fa3e1 | ||
|
|
17cd27b79c | ||
|
|
323f34b83b | ||
|
|
625c7b02c5 | ||
|
|
f27b2f184f | ||
|
|
ed24fd93a8 | ||
|
|
652f2a8362 | ||
|
|
3864c9cb01 | ||
|
|
353aeadcd6 | ||
|
|
514175fd53 | ||
|
|
82d6e76c80 | ||
|
|
be6e8ccb3e | ||
|
|
e49ecef01a | ||
|
|
709a030c7e | ||
|
|
b07c7b3ca2 | ||
|
|
84983bc07d | ||
|
|
77644ff24d | ||
|
|
e32ec3da66 | ||
|
|
7b7eb919c9 | ||
|
|
351d997ba7 | ||
|
|
130d02e82e | ||
|
|
ce8b399dcb | ||
|
|
ddd2ce401e | ||
|
|
4f310a8764 | ||
|
|
69525c1d60 | ||
|
|
26775be49b | ||
|
|
3615519fdf | ||
|
|
8722df9dea | ||
|
|
b0a34e1558 | ||
|
|
36e7509fc1 | ||
|
|
513bc0909b | ||
|
|
f64c408653 | ||
|
|
7832ce6ba0 | ||
|
|
c01f11bd7f | ||
|
|
4b3c7cebb3 | ||
|
|
8e70f422a2 | ||
|
|
ab18a8e275 | ||
|
|
7b73b0c195 | ||
|
|
876f867644 | ||
|
|
258fefd64f | ||
|
|
2de48d2caa | ||
|
|
58a64fbcb2 | ||
|
|
813d2f6fc9 | ||
|
|
79287fdeba | ||
|
|
c979604973 | ||
|
|
527f248106 | ||
|
|
8894b951df | ||
|
|
b21e29fb73 | ||
|
|
9193e3bcfd | ||
|
|
a3e8dbe136 | ||
|
|
6756e4a071 | ||
|
|
9d6bf8fed4 | ||
|
|
ee17b7a417 | ||
|
|
469251108e | ||
|
|
bd5edde109 | ||
|
|
073dc37b7b | ||
|
|
0e1c69e54f | ||
|
|
c41a0b754b | ||
|
|
fda002fd64 | ||
|
|
6e3bc0e1f3 | ||
|
|
6f059af2d7 | ||
|
|
1d3d4e3c8f | ||
|
|
03da23d1d5 | ||
|
|
e0bccccf0e | ||
|
|
934350ee6a | ||
|
|
fb89e34485 | ||
|
|
713e003d69 | ||
|
|
0ad8ffe098 | ||
|
|
04a3cdfef6 | ||
|
|
b0f33f8bd4 | ||
|
|
e50af063e5 | ||
|
|
db3adbf8b6 | ||
|
|
9d2aeddbb7 | ||
|
|
9adc23e78a | ||
|
|
adf3000b24 | ||
|
|
89bb210d29 | ||
|
|
e29a823a9c | ||
|
|
45866c886f | ||
|
|
74db8573eb | ||
|
|
b4eaeda5b0 | ||
|
|
8b2925951f | ||
|
|
b76ac05048 | ||
|
|
6650d995e5 | ||
|
|
22549189ac | ||
|
|
06f2966639 | ||
|
|
7d11789354 | ||
|
|
b017cebc2b | ||
|
|
b5e4ca5142 | ||
|
|
744685644c | ||
|
|
ea6a637847 | ||
|
|
e635f50580 | ||
|
|
ae53af9187 | ||
|
|
cba4b12f5b | ||
|
|
6527b579b3 | ||
|
|
5c95a18f69 | ||
|
|
f339ade838 | ||
|
|
499130500b | ||
|
|
fed439e5d8 | ||
|
|
ada22b22c9 | ||
|
|
165a33a470 | ||
|
|
65b0dbacca | ||
|
|
5948adf0df | ||
|
|
12e7966dd6 | ||
|
|
5c8bc5914d | ||
|
|
5fda65c4b0 | ||
|
|
a306dfc748 | ||
|
|
ed0c191325 | ||
|
|
9c4e8d93f2 | ||
|
|
97e0ecee7e | ||
|
|
76e97c23b4 | ||
|
|
14687f8f3c | ||
|
|
86a3f44391 | ||
|
|
db36543e15 | ||
|
|
fbbf6c7f80 | ||
|
|
690700b094 | ||
|
|
1060b5a914 | ||
|
|
db46f8d13d | ||
|
|
4283a1c29f | ||
|
|
1d751a4196 | ||
|
|
a7b4cb4aca | ||
|
|
dc381f1ae3 | ||
|
|
d58654052f | ||
|
|
061385a6a3 | ||
|
|
0dc8c850a6 | ||
|
|
d61e8f13c1 | ||
|
|
0a82473074 | ||
|
|
8f85633368 | ||
|
|
4516009dbd | ||
|
|
62941f3322 | ||
|
|
faf056464b | ||
|
|
1eeed55335 | ||
|
|
34447423ef | ||
|
|
40df81961c | ||
|
|
0e82832c46 | ||
|
|
ceedadc3bd | ||
|
|
b7a61f1471 | ||
|
|
d58a839f4b | ||
|
|
3d2b941baa | ||
|
|
0cf07ee633 | ||
|
|
8c2e22958e | ||
|
|
b3bd1814f2 | ||
|
|
5b4cf11fe1 | ||
|
|
721e53d206 | ||
|
|
8b72b8a0ff | ||
|
|
c0671acf92 | ||
|
|
130d446664 | ||
|
|
e656c11712 | ||
|
|
20caf89894 | ||
|
|
3747e33e7c | ||
|
|
d1e533f23d | ||
|
|
b7f7167070 | ||
|
|
c23be1b5b7 | ||
|
|
343a5dcf20 | ||
|
|
ad52459f8d | ||
|
|
8db769a308 | ||
|
|
682caa82ae | ||
|
|
5356dd523a | ||
|
|
35e373ee37 | ||
|
|
9c69aab450 | ||
|
|
4ef6e7e23d | ||
|
|
802ab06db4 | ||
|
|
cd0b0a4c37 | ||
|
|
3832485f0f | ||
|
|
fc5c12431e | ||
|
|
d068e0a73a | ||
|
|
7ff8e34685 | ||
|
|
6afe1a7f0e | ||
|
|
04aa6d5fdf | ||
|
|
f47ac7311f | ||
|
|
23523355b7 | ||
|
|
69b98c9375 | ||
|
|
c0eb3a0814 | ||
|
|
64f7dd69a2 | ||
|
|
4e969d7bcc | ||
|
|
b8f13bffe9 | ||
|
|
1d6164c2d5 | ||
|
|
446b18b603 | ||
|
|
72ddbb8020 | ||
|
|
0e284d69c3 | ||
|
|
81c4bd6576 | ||
|
|
eb5c666d8d | ||
|
|
202f407d2c | ||
|
|
43b8d665f3 | ||
|
|
7e2b3582e1 | ||
|
|
16dd2828b4 | ||
|
|
778011d219 | ||
|
|
22d4574d54 | ||
|
|
66ae2f6c88 | ||
|
|
f094cf7a2a | ||
|
|
c4d242fa6a | ||
|
|
aac09ca2a3 | ||
|
|
bf2a36868b | ||
|
|
435cc244aa | ||
|
|
95c7899533 | ||
|
|
cddb70aa56 | ||
|
|
a571f59693 | ||
|
|
5e3edd3f41 | ||
|
|
585626c269 | ||
|
|
c64801e73d | ||
|
|
2991c30489 | ||
|
|
cae4d75765 | ||
|
|
0f6c22d9c7 | ||
|
|
e403551b18 | ||
|
|
f114f1c6d1 | ||
|
|
a6723173fd | ||
|
|
457076998a | ||
|
|
d21cadcbe3 | ||
|
|
388b76446e | ||
|
|
584fca5f0d | ||
|
|
01c57a50d3 | ||
|
|
f6ca255355 | ||
|
|
9e1a460dc7 | ||
|
|
00b83ff531 | ||
|
|
5c2950d838 | ||
|
|
155667833e | ||
|
|
6b78364596 | ||
|
|
35b79cf649 | ||
|
|
fc2efb13eb | ||
|
|
84361b4d87 | ||
|
|
40c842b0d7 | ||
|
|
f76581b860 | ||
|
|
b3dba8b3cf | ||
|
|
8174b68c0d | ||
|
|
dea7bdc7fe | ||
|
|
c165be6c2a | ||
|
|
d99b5dbff5 | ||
|
|
cc50a50a0d | ||
|
|
dbcd3f3c0d | ||
|
|
67c11b9128 | ||
|
|
cd40699293 | ||
|
|
70015568e2 | ||
|
|
767ce6b073 | ||
|
|
09818174eb | ||
|
|
ebfd3c73e3 | ||
|
|
cc60f8453b | ||
|
|
0cf286a063 | ||
|
|
3da977a96f | ||
|
|
c10761ec9a | ||
|
|
b0c6324713 | ||
|
|
1b51c19c97 | ||
|
|
a83cb0f50f | ||
|
|
d6022cb587 | ||
|
|
67f525a603 | ||
|
|
634006aa7e | ||
|
|
3eb3f926f5 | ||
|
|
47caa5bdf8 | ||
|
|
67c3727362 | ||
|
|
b510d6e5ea | ||
|
|
1ab27bd556 | ||
|
|
9600a5e56f | ||
|
|
6c0d5c0db4 | ||
|
|
4b010e6523 | ||
|
|
8962253dd2 | ||
|
|
afd89e4309 | ||
|
|
43332f49db | ||
|
|
cc90de5541 | ||
|
|
5e1460bf29 | ||
|
|
7dbdca849a | ||
|
|
df525d2d32 | ||
|
|
dc96f1766b | ||
|
|
70da7d5dce | ||
|
|
0f1169ce36 | ||
|
|
1c38d01abb | ||
|
|
f82b5add11 | ||
|
|
8e2c1f3b07 | ||
|
|
8ef92d70d2 | ||
|
|
b5ce5c9e9d | ||
|
|
a19a399acd | ||
|
|
529d161478 | ||
|
|
168f78ef23 | ||
|
|
a159127572 | ||
|
|
88933ce0bb | ||
|
|
6d703f7eae | ||
|
|
f4a0fe3445 | ||
|
|
b174c8622c | ||
|
|
98e0412ee6 | ||
|
|
a138238994 | ||
|
|
01f5cb1fd6 | ||
|
|
472554bc55 | ||
|
|
ad9a4ab100 | ||
|
|
a806ca1319 | ||
|
|
aac5bafe43 | ||
|
|
668106d445 | ||
|
|
a74671abdd | ||
|
|
120d494c47 | ||
|
|
5b1d01e0c5 | ||
|
|
950138a340 | ||
|
|
93d8f92165 | ||
|
|
488df340a1 | ||
|
|
dc51ce7bef | ||
|
|
862f32e699 | ||
|
|
d0f6a04661 | ||
|
|
cb3fad6a2c | ||
|
|
c88c232889 | ||
|
|
6b03e4bb5f | ||
|
|
7727d63451 | ||
|
|
3e567e5be6 | ||
|
|
4ae6ba9a3c | ||
|
|
360abcc61d | ||
|
|
41e57db032 | ||
|
|
b62137d185 | ||
|
|
2bd635e1d4 | ||
|
|
8e8ab80aa0 | ||
|
|
7f4c2e0487 | ||
|
|
e52e715a53 | ||
|
|
e3e7377d86 | ||
|
|
625dbe9624 | ||
|
|
42f0a74edf | ||
|
|
cc9f53d71a | ||
|
|
2f122dfb05 | ||
|
|
f7b0010180 | ||
|
|
2c0fecaa62 | ||
|
|
548966906c | ||
|
|
6473546f56 | ||
|
|
035d63c880 | ||
|
|
d56176dfa5 | ||
|
|
c1815f5107 | ||
|
|
7ac175e726 | ||
|
|
eb5e843251 | ||
|
|
bc098aa4d2 | ||
|
|
bcd394b47f | ||
|
|
fd2202fcaf | ||
|
|
a7629e317a | ||
|
|
3a378f8710 | ||
|
|
cf59907fcd | ||
|
|
624ba8dc8d | ||
|
|
fb4b665c85 | ||
|
|
5a9f371e9d | ||
|
|
5c3d1a04de | ||
|
|
024b44f477 | ||
|
|
35046f8a06 | ||
|
|
23c2f85e8a | ||
|
|
56226c1d52 | ||
|
|
9014f117d3 | ||
|
|
e073bfee91 | ||
|
|
897d5d4220 | ||
|
|
4228605a3d | ||
|
|
e7131b1881 | ||
|
|
8a22797bca | ||
|
|
b9e569a1e0 | ||
|
|
1284b4b46b | ||
|
|
175c289e66 | ||
|
|
899e92882d | ||
|
|
d516b3a15d | ||
|
|
7678c3f139 | ||
|
|
3b49c0cb75 | ||
|
|
32d784e57e | ||
|
|
d8f8444275 | ||
|
|
41676c1f13 | ||
|
|
a3def497b4 | ||
|
|
381db85149 | ||
|
|
d0daa2eec5 | ||
|
|
9b5f0f7fce | ||
|
|
b3ef3d8a6f | ||
|
|
bd1924b016 | ||
|
|
1c781d5bc5 | ||
|
|
586aa53e84 | ||
|
|
c5f0389138 | ||
|
|
b93613c25e | ||
|
|
a8b0c1024d | ||
|
|
25a63ccb25 | ||
|
|
5cdfe545b5 | ||
|
+16 |
9be7295084 | ||
|
|
6905bcc986 | ||
|
|
1050032a13 | ||
|
|
9b223dd217 | ||
|
|
0a1b5961e9 | ||
|
|
a8a70b76d7 | ||
|
+1 |
05b63903b5 | ||
|
|
7d8b55dfb8 | ||
|
|
72a3e59ca6 | ||
|
|
3ab94a751b | ||
|
|
66049f189b | ||
|
|
69ec3f8751 | ||
|
|
feb43c5678 | ||
|
|
1fae67282d | ||
|
|
886f0b74f6 | ||
|
|
32e1a43aa5 | ||
|
|
dcd806802e | ||
|
|
5746cbbbd7 | ||
|
|
e23903fd0c | ||
|
|
780710f226 | ||
|
|
4607ede541 | ||
|
|
67ad4b04ef | ||
|
|
6432d689dd | ||
|
|
2eb856fffb | ||
|
|
56312831ca | ||
|
|
fe54d42bc0 | ||
|
|
2ea24c1b3a | ||
|
|
e0040e2ede | ||
|
|
0ac0fd4a8d | ||
|
|
0ef87bcc08 | ||
|
|
660bda6e14 | ||
|
|
242cf40135 | ||
|
|
7319c55430 | ||
|
|
5ea29f610b | ||
|
|
b74aff0500 | ||
|
|
d2e8f2f4aa | ||
|
|
28681a1f9e | ||
|
|
6814c7c357 | ||
|
|
f475540508 | ||
|
|
44660a5042 | ||
|
|
339386fd2b | ||
|
|
faa7cb71db | ||
|
|
014a376c30 | ||
|
|
377172e44a | ||
|
|
5b9e19cc30 | ||
|
|
0457ec7d16 | ||
|
|
2947ee218e | ||
|
|
4e841a9fb1 | ||
|
|
b7ecb9b2ec | ||
|
|
7e1873bc10 | ||
|
|
8eb60210a3 | ||
|
|
d608f732f7 | ||
|
|
2d9a2955ef | ||
|
|
cae4423f30 | ||
|
|
f0e53a9fb3 | ||
|
|
79da1a198b | ||
|
|
64cb804147 | ||
|
|
344cb25229 | ||
|
|
74d21a7acd | ||
|
|
f315bf9607 | ||
|
|
810d54bef5 | ||
|
|
48eafa1382 | ||
|
|
4b6c1e0f08 | ||
|
|
edfd857030 | ||
|
|
408a06d3eb | ||
|
|
fd3a2f0569 | ||
|
|
7d8ba72717 | ||
|
|
f936b50ce9 | ||
|
|
93f5da2d02 | ||
|
|
bc06c268ca | ||
|
|
9e6f2762c0 | ||
|
|
6ac8a42bd3 | ||
|
|
89647d96c7 | ||
|
|
0b0fac4a41 | ||
|
|
f59e15bbbe | ||
|
|
b7398b1bf9 | ||
|
|
0082e724bc | ||
|
|
57a2daf30d | ||
|
|
db90f29cd0 | ||
|
|
00fd8fa1da | ||
|
|
60100eb980 | ||
|
|
a6eb6be1ab | ||
|
|
5eff094af6 | ||
|
|
3b126f53d2 | ||
|
|
1a5b722994 | ||
|
|
5fe20dff05 | ||
|
|
04cfdae1e5 | ||
|
|
8776f98cf5 | ||
|
|
da484aef26 | ||
|
|
e8ebc65053 | ||
|
|
bd08de41d5 | ||
|
|
18f8a541f0 | ||
|
|
007ecd2db1 | ||
|
|
89aaad2401 | ||
|
|
17d836752e | ||
|
|
3949b73751 | ||
|
|
2731504278 | ||
|
|
babbfaea68 | ||
|
|
9f7b9994b8 | ||
|
|
d156c10aed | ||
|
|
9f3329c85f | ||
|
|
fd1c58f68d | ||
|
|
6d1aa503df | ||
|
|
a58ad35c8b | ||
|
|
64cd5f9658 | ||
|
|
a13dd89fd2 | ||
|
|
2f5cfa9d5d | ||
|
|
a9dc7bf7f3 | ||
|
|
c1492deaa0 | ||
|
|
2252b2a769 | ||
|
|
73f71503f5 | ||
|
|
2e224ce626 | ||
|
|
e5406d57c6 | ||
|
|
433a83303f | ||
|
|
f309d5eafd | ||
|
|
986103395b | ||
|
|
d569721e01 | ||
|
|
08cac3c6cc | ||
|
|
f80d066851 | ||
|
|
d6584093f5 | ||
|
|
fb03a57725 | ||
|
|
779272d65f | ||
|
|
c21a2952a5 | ||
|
|
b46d049158 | ||
|
|
dc52c88d88 | ||
|
|
df4e947c8a | ||
|
|
a4ec6c1f66 | ||
|
|
a8dd57d009 | ||
|
|
af3eabdd1c | ||
|
|
d6b300d3df | ||
|
|
b5789538e6 | ||
|
|
d6c5b2b8a2 | ||
|
|
3a04647c3c | ||
|
|
d2999c1aa6 | ||
|
|
b3dbc7092a | ||
|
|
edb3cb6b6a | ||
|
|
4dd78b2e98 | ||
|
|
2f91f56b7b | ||
|
|
be3dac488b | ||
|
|
553bdbaf9f | ||
|
|
40d3f1243b | ||
|
|
740e3f8385 | ||
|
|
47d089dff7 | ||
|
|
c6f1f07155 | ||
|
|
0ebd3499b7 | ||
|
|
c88d4a7b0e | ||
|
|
8bebae9db6 | ||
|
|
b6b1a48a5c | ||
|
|
63ed50ef45 | ||
|
|
e63715d1b0 | ||
|
|
edf3eac6d6 | ||
|
|
743f285c8d | ||
|
|
458433464c | ||
|
|
64740a4f85 | ||
|
|
b5a2bce261 | ||
|
|
5e351c854b | ||
|
|
0875bea046 | ||
|
|
9ad5f77816 | ||
|
|
998f9059bc | ||
|
|
a1439c3ec2 | ||
|
|
742b6f6755 | ||
|
|
5fdb34598c | ||
|
|
4f982d1ab0 | ||
|
|
4a28056d9f | ||
|
|
3f2fa71e22 | ||
|
|
08c81bf62f | ||
|
|
e16d6cb4ba | ||
|
|
a54d14e803 | ||
|
|
01ae281769 | ||
|
|
9cb10ff14d | ||
|
|
62f40d3ad1 | ||
|
|
57601ef989 | ||
|
|
0a05edbec5 | ||
|
|
fcef24fdbf | ||
|
|
e45c31abfb | ||
|
|
198c190ca7 | ||
|
|
392a59f9a9 | ||
|
|
c01b7f7bce | ||
|
|
0323b044b8 | ||
|
|
256302fdcf | ||
|
|
3f3330af41 | ||
|
|
33342baa3f | ||
|
|
563cbd7943 | ||
|
|
ddb1601290 | ||
|
|
8e9d3c09f3 | ||
|
|
ade04d2cf2 | ||
|
|
53555c88c7 | ||
|
|
03bf31269d |
+219
-10
@@ -6,7 +6,7 @@
|
||||
"files": [
|
||||
"README.md",
|
||||
"README_CN.md",
|
||||
"home/src/pages/team/index.jsx"
|
||||
"README_JP.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": true,
|
||||
@@ -1269,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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1441,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": [
|
||||
@@ -2504,6 +2500,219 @@
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "RainBondsongyg",
|
||||
"name": "songyg",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/94501396?v=4",
|
||||
"profile": "https://github.com/RainBondsongyg",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lx1229",
|
||||
"name": "Liuxin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/44620005?v=4",
|
||||
"profile": "https://github.com/lx1229",
|
||||
"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,
|
||||
|
||||
@@ -16,11 +16,15 @@
|
||||
#
|
||||
|
||||
github:
|
||||
description: Apache HertzBeat(incubating) is a real-time monitoring 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
|
||||
@@ -52,6 +52,9 @@ github:
|
||||
required_pull_request_reviews:
|
||||
dismiss_stale_reviews: true
|
||||
required_approving_review_count: 1
|
||||
# enable GitHub Dependabot to create PRs for security alerts but not for every dependency update
|
||||
dependabot_alerts: true
|
||||
dependabot_updates: false
|
||||
notifications:
|
||||
commits: notifications@hertzbeat.apache.org
|
||||
issues: notifications@hertzbeat.apache.org
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {},
|
||||
// same extensions as Gitpod, should match /.gitpod.yml
|
||||
"extensions": [
|
||||
"vscjava.vscode-java-pack",
|
||||
"editorconfig.editorconfig",
|
||||
|
||||
@@ -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
|
||||
Executable
+23
@@ -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
|
||||
Executable
+24
@@ -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
@@ -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:
|
||||
|
||||
@@ -20,33 +20,74 @@
|
||||
|
||||
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: mvn clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
|
||||
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
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -49,11 +49,9 @@ jobs:
|
||||
- name: Dead Link Check
|
||||
run: |
|
||||
sudo npm install -g markdown-link-check@3.8.7
|
||||
for file in $(find ./home -name "*.md"); do
|
||||
if ! grep -Fxq "$file" ./script/ci/exclude_files.txt; then
|
||||
markdown-link-check -c ./script/ci/link_check.json -q "$file"
|
||||
fi
|
||||
done
|
||||
find ./home -name "*.md" > all_md_files.txt
|
||||
grep -vFf ./script/ci/exclude_files.txt all_md_files.txt > to_check.txt
|
||||
xargs -P 8 -a to_check.txt -I{} markdown-link-check -c ./script/ci/link_check.json -q "{}"
|
||||
|
||||
- name: NPM INSTALL
|
||||
working-directory: home
|
||||
|
||||
@@ -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 }}"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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/
|
||||
|
||||
Vendored
+2
-2
@@ -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
@@ -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
|
||||
|
||||
@@ -39,6 +39,7 @@ header:
|
||||
- '**/*.js'
|
||||
- '**/*.less'
|
||||
- '**/*.txt'
|
||||
- '**/*.st'
|
||||
- '**/target/**'
|
||||
- '.gitattributes'
|
||||
- '**/.gitignore'
|
||||
@@ -66,7 +67,9 @@ header:
|
||||
- '.gitpod.yml'
|
||||
- 'karma.conf.js'
|
||||
- 'proxy.conf.js'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.helmignore'
|
||||
- 'script/helm/**'
|
||||
- 'web-app/src/app/core/**'
|
||||
- 'web-app/src/app/layout/**'
|
||||
- 'web-app/src/app/routes/exception/**'
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||

|
||||
|
||||
<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)** 提供可视化控制台页面
|
||||
> 监控告警系统可视化控制台前端
|
||||
|
||||

|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
Apache HertzBeat (incubating) is an effort undergoing incubation at the Apache
|
||||
Software Foundation (ASF), sponsored by the Apache Incubator PMC.
|
||||
|
||||
Incubation is required of all newly accepted projects until a further review
|
||||
indicates that the infrastructure, communications, and decision making process
|
||||
have stabilized in a manner consistent with other successful ASF projects.
|
||||
|
||||
While incubation status is not necessarily a reflection of the completeness
|
||||
or stability of the code, it does indicate that the project has yet to be
|
||||
fully endorsed by the ASF.
|
||||
@@ -1,4 +1,4 @@
|
||||
Apache HertzBeat (incubating)
|
||||
Apache HertzBeat™
|
||||
Copyright 2024-2025 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
|
||||
@@ -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 monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
[](https://x.com/hertzbeat1024)
|
||||
@@ -21,6 +19,7 @@
|
||||
[](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||
[](https://gitpod.io/#https://github.com/apache/hertzbeat)
|
||||
[](https://deepwiki.com/apache/hertzbeat)
|
||||
[](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) (incubating) is an easy-to-use, open source, real-time monitoring 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.
|
||||
|
||||
----
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
----
|
||||
|
||||
## 🥐 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
|
||||
|
||||
##### 3:Start via source code
|
||||
|
||||
1. Local source code debugging needs to start the back-end project `manager` and the front-end project `web-app`.
|
||||
2. Backend:need `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. Web:need `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>
|
||||
@@ -527,6 +524,35 @@ Thanks to these wonderful people, welcome to join us:
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
@@ -563,15 +589,15 @@ 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
|
||||
|
||||
<p align="left">
|
||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||
<br /><br />
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||
</p>
|
||||
|
||||
## 🛡️ License
|
||||
|
||||
+49
-27
@@ -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,自定义监控和状态页构建能力。
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
[](https://x.com/hertzbeat1024)
|
||||
@@ -28,29 +26,24 @@
|
||||
|
||||
## 🎡 <font color="green">介绍</font>
|
||||
|
||||
[Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating)是一个易用友好的开源实时监控告警系统,无需 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智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求。
|
||||
|
||||
----
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
----
|
||||
|
||||
## 🥐 模块
|
||||
@@ -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酱`。
|
||||
|
||||
## 🐕 快速开始
|
||||
@@ -126,7 +119,7 @@
|
||||
|
||||
- `-e IDENTITY=custom-collector-name` : 配置此采集器的唯一性标识符名称,多个采集器名称不能相同,建议自定义英文名称。
|
||||
- `-e MODE=public` : 配置运行模式(public or private), 公共集群模式或私有云边模式。
|
||||
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertaBeat 服务的对外 IP。
|
||||
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertzBeat 服务的对外 IP。
|
||||
- `-e MANAGER_PORT=1158` : 配置连接主 HertzBeat 服务的对外端口,默认1158。
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -525,6 +518,35 @@ Thanks these wonderful people, welcome to join us:
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
@@ -562,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
|
||||
@@ -570,8 +592,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
||||
<p align="left">
|
||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||
<br /><br />
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||
</p>
|
||||
|
||||
## 🛡️ License
|
||||
|
||||
+63
-38
@@ -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>
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
@@ -22,44 +20,38 @@
|
||||
[](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 インテリジェンス、強力なカスタマイズ、多種類サポート、高性能、容易な拡張性は、ユーザーが迅速かつ便利に観測要件を実現することを支援することを目的としています。
|
||||
|
||||
----
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
----
|
||||
|
||||
## 🥐 モジュール
|
||||
## 🥐 モジュール
|
||||
|
||||

|
||||

|
||||
|
||||
## ⛄ 利用可能
|
||||
## ⛄ 利用可能
|
||||
|
||||
> `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>
|
||||
@@ -525,6 +517,39 @@ Thanks these wonderful people, welcome to join us:
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
@@ -532,7 +557,7 @@ Thanks these wonderful people, welcome to join us:
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
## 💬 コミュニティ交流
|
||||
|
||||
@@ -562,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
|
||||
@@ -570,8 +595,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
||||
<p align="left">
|
||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||
<br /><br />
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||
</p>
|
||||
|
||||
## 🛡️ License
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Security Policy
|
||||
|
||||
Apache HertzBeat follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
|
||||
Apache HertzBeat™ follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
|
||||
|
||||
## Supported Versions
|
||||
|
||||
@@ -17,6 +17,6 @@ To report a new vulnerability you have discovered please follow the [ASF vulnera
|
||||
|
||||
## Extra Information
|
||||
|
||||
- [Apache HertzBeat Security Model](https://hertzbeat.apache.org/docs/help/security_model)
|
||||
- [Apache HertzBeat™ Security Model](https://hertzbeat.apache.org/docs/help/security_model)
|
||||
- [ASF Security](https://www.apache.org/security/)
|
||||
- [Apache HertzBeat Document](https://hertzbeat.apache.org/docs/)
|
||||
- [Apache HertzBeat™ Document](https://hertzbeat.apache.org/docs/)
|
||||
|
||||
@@ -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 能力继续负责这些目标,独立需求另行评审;
|
||||
- OpAMP;Manager 到 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;
|
||||
- 停压恢复与无残留长任务证明。
|
||||
|
||||
### M8:Java 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 Runtime,Java Agentless 不停且 Go 自动恢复;
|
||||
10. 重启整个 Collector,offset、队列、配置 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”作为结论。
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "ftp",
|
||||
"host": "127.0.0.1",
|
||||
"instance": "127.0.0.1",
|
||||
"name": "{{.param.monitorFTP}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "api",
|
||||
"host": "127.0.0.1",
|
||||
"instance": "127.0.0.1",
|
||||
"name": "{{.param.monitorHTTP}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "ping",
|
||||
"host": "127.0.0.1",
|
||||
"instance": "127.0.0.1",
|
||||
"name": "{{.param.monitorPing}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "port",
|
||||
"host": "127.0.0.1",
|
||||
"instance": "127.0.0.1",
|
||||
"name": "{{.param.monitorPort}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "fullsite",
|
||||
"host": "127.0.0.1",
|
||||
"instance": "127.0.0.1",
|
||||
"name": "{{.param.monitorSiteMap}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "ssl_cert",
|
||||
"host": "127.0.0.1",
|
||||
"instance": "127.0.0.1",
|
||||
"name": "{{.param.monitorSSL}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "udp_port",
|
||||
"host": "127.0.0.1",
|
||||
"instance": "127.0.0.1",
|
||||
"name": "{{.param.monitorUDP}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "website",
|
||||
"host": "127.0.0.1",
|
||||
"instance": "127.0.0.1",
|
||||
"name": "{{.param.monitorWebsite}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -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
@@ -57,7 +57,7 @@ items:
|
||||
"credential": "hertzbeat"
|
||||
}
|
||||
expect:
|
||||
statusCode: 409
|
||||
statusCode: 500
|
||||
- name: monitorList
|
||||
request:
|
||||
api: /api/monitors?pageIndex=0&pageSize=8
|
||||
|
||||
@@ -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>
|
||||
+254
@@ -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) {
|
||||
}
|
||||
}
|
||||
+479
@@ -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 : "");
|
||||
}
|
||||
|
||||
}
|
||||
+168
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
@@ -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;
|
||||
}
|
||||
}
|
||||
+228
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
@@ -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);
|
||||
}
|
||||
}
|
||||
+52
@@ -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
|
||||
}
|
||||
}
|
||||
+90
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+200
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -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) {
|
||||
}
|
||||
}
|
||||
+159
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+506
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+214
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+213
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+120
@@ -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);
|
||||
}
|
||||
}
|
||||
+116
@@ -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();
|
||||
}
|
||||
}
|
||||
+419
@@ -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
|
||||
}
|
||||
}
|
||||
+90
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
+199
@@ -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 {
|
||||
}
|
||||
}
|
||||
+184
@@ -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();
|
||||
}
|
||||
}
|
||||
+83
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+282
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+153
@@ -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;
|
||||
}
|
||||
}
|
||||
+57
@@ -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) {
|
||||
}
|
||||
}
|
||||
+164
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+35
@@ -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;
|
||||
}
|
||||
}
|
||||
+174
@@ -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();
|
||||
}
|
||||
}
|
||||
+221
@@ -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();
|
||||
}
|
||||
}
|
||||
+217
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
@@ -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;
|
||||
}
|
||||
+52
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -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;
|
||||
}
|
||||
}
|
||||
+60
@@ -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();
|
||||
}
|
||||
}
|
||||
+43
@@ -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);
|
||||
}
|
||||
}
|
||||
+43
@@ -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;
|
||||
}
|
||||
+87
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.Pattern;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** Manager-issued authority snapshot for a canonical Agent target. */
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AgentTargetAuthority {
|
||||
|
||||
@Positive
|
||||
private Long bindingId;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 64)
|
||||
private String version;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = "sha256:[0-9a-f]{64}")
|
||||
private String hash;
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.Valid;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* HertzBeat resource target referenced by an Agent Gateway request.
|
||||
*/
|
||||
@Data
|
||||
@Builder(toBuilder = true)
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AgentTargetRef {
|
||||
|
||||
@Size(max = 64)
|
||||
private String version;
|
||||
|
||||
@Positive
|
||||
private Long monitorId;
|
||||
|
||||
@Positive
|
||||
private Long alertId;
|
||||
|
||||
@Size(max = 16)
|
||||
private String alertType;
|
||||
|
||||
@Positive
|
||||
private Long entityId;
|
||||
|
||||
@Size(max = 128)
|
||||
private String collector;
|
||||
|
||||
@Valid
|
||||
private AgentSignalRef signal;
|
||||
|
||||
@Valid
|
||||
private AgentTopologyRef topology;
|
||||
|
||||
@Valid
|
||||
private AgentTraceRef trace;
|
||||
|
||||
@Valid
|
||||
private AgentLogRef log;
|
||||
|
||||
@Valid
|
||||
private AgentServiceRef service;
|
||||
|
||||
@Valid
|
||||
private AgentTargetAuthority authority;
|
||||
}
|
||||
+90
@@ -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.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.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Topology scope and selection selected by an operator.
|
||||
*/
|
||||
@Data
|
||||
@Builder(toBuilder = true)
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AgentTopologyRef {
|
||||
|
||||
@Positive
|
||||
private Long rootEntityId;
|
||||
|
||||
@Size(max = 512)
|
||||
private String nodeId;
|
||||
|
||||
@Size(max = 512)
|
||||
private String edgeId;
|
||||
|
||||
@Min(1)
|
||||
@Max(2)
|
||||
private Integer depth;
|
||||
|
||||
@Size(max = 128)
|
||||
private String environment;
|
||||
|
||||
@Size(max = 64)
|
||||
private String sourceKind;
|
||||
|
||||
@Positive
|
||||
private Long start;
|
||||
|
||||
@Positive
|
||||
private Long end;
|
||||
|
||||
@Size(max = 128)
|
||||
private String relationType;
|
||||
|
||||
private Boolean hideInternal;
|
||||
|
||||
@PositiveOrZero
|
||||
@Max(10_000)
|
||||
private Integer pageIndex;
|
||||
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
private Integer pageSize;
|
||||
|
||||
@AssertTrue
|
||||
@JsonIgnore
|
||||
public boolean isSingleSelectionValid() {
|
||||
return nodeId == null || edgeId == null;
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
@JsonIgnore
|
||||
public boolean isTimeWindowValid() {
|
||||
return start == null && end == null || start != null && end != null && start < end;
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** Exact Trace Explore detail scope selected by an operator. */
|
||||
@Data
|
||||
@Builder(toBuilder = true)
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AgentTraceRef {
|
||||
|
||||
@NotBlank
|
||||
@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;
|
||||
|
||||
@Positive
|
||||
private Long start;
|
||||
|
||||
@Positive
|
||||
private Long end;
|
||||
|
||||
@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;
|
||||
|
||||
@PositiveOrZero
|
||||
private Long minDurationMs;
|
||||
|
||||
@PositiveOrZero
|
||||
private Long maxDurationMs;
|
||||
|
||||
@AssertTrue
|
||||
@JsonIgnore
|
||||
public boolean isTimeWindowValid() {
|
||||
return start != null && end != null && start < end;
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
@JsonIgnore
|
||||
public boolean isDurationRangeValid() {
|
||||
return minDurationMs == null || maxDurationMs == null || minDurationMs <= maxDurationMs;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.Valid;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.util.Objects;
|
||||
import lombok.Builder;
|
||||
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
|
||||
import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Trusted invocation metadata supplied by a channel boundary.
|
||||
*/
|
||||
@Builder(toBuilder = true)
|
||||
public record GatewayEnvelope(
|
||||
@Size(max = 64) String channelId,
|
||||
Long receivedAt,
|
||||
@Valid AgentActor actor,
|
||||
@Size(max = 16) String preferredLanguage,
|
||||
@Size(max = 128) String workspaceId) {
|
||||
|
||||
public GatewayEnvelope {
|
||||
if (!StringUtils.hasText(channelId)) {
|
||||
throw new IllegalArgumentException("Gateway envelope channel id is required");
|
||||
}
|
||||
receivedAt = Objects.requireNonNull(receivedAt, "Gateway envelope received time is required");
|
||||
if (receivedAt < 0) {
|
||||
throw new IllegalArgumentException("Gateway envelope received time must not be negative");
|
||||
}
|
||||
workspaceId = AuthTokenScopes.normalizeWorkspaceId(workspaceId);
|
||||
}
|
||||
|
||||
public String getChannelId() {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
public Long getReceivedAt() {
|
||||
return receivedAt;
|
||||
}
|
||||
|
||||
public AgentActor getActor() {
|
||||
return actor;
|
||||
}
|
||||
|
||||
public String getPreferredLanguage() {
|
||||
return preferredLanguage;
|
||||
}
|
||||
|
||||
public String getWorkspaceId() {
|
||||
return workspaceId;
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.Valid;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Normalized user input accepted by Agent Gateway channels.
|
||||
*/
|
||||
@Getter
|
||||
public class UserInput {
|
||||
|
||||
@Size(max = 128)
|
||||
private final String messageId;
|
||||
|
||||
@Size(max = 256)
|
||||
private final String conversationId;
|
||||
|
||||
@Valid
|
||||
private final AgentTargetRef target;
|
||||
|
||||
@Valid
|
||||
private final AgentAlertIncidentContext alertIncident;
|
||||
|
||||
@Valid
|
||||
private final Message message;
|
||||
|
||||
@Builder(toBuilder = true)
|
||||
private UserInput(String messageId, String conversationId, AgentTargetRef target,
|
||||
AgentAlertIncidentContext alertIncident, Message message) {
|
||||
if (!StringUtils.hasText(conversationId)) {
|
||||
throw new IllegalArgumentException("Agent input conversation id is required");
|
||||
}
|
||||
this.messageId = messageId;
|
||||
this.conversationId = conversationId;
|
||||
this.target = target;
|
||||
this.alertIncident = alertIncident;
|
||||
// UserInput is the channel-normalization boundary consumed by session, run, transcript, and runtime services.
|
||||
this.message = Objects.requireNonNull(message, "Agent input message is required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized message content owned by one user input.
|
||||
*/
|
||||
@Builder
|
||||
public record Message(String text, List<String> attachments) {
|
||||
|
||||
public Message {
|
||||
if (!StringUtils.hasText(text)) {
|
||||
throw new IllegalArgumentException("Agent message text is required");
|
||||
}
|
||||
attachments = attachments == null ? List.of() : List.copyOf(attachments);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public List<String> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -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.conversation;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
|
||||
|
||||
/** Safe owner-scoped WebUI request material used for explicit fresh-ID retry. */
|
||||
public record AgentRetryRequest(
|
||||
String conversationId,
|
||||
String messageId,
|
||||
String message,
|
||||
AgentTargetRef target,
|
||||
List<String> attachments,
|
||||
String preferredLanguage) {
|
||||
|
||||
public AgentRetryRequest {
|
||||
attachments = attachments == null ? List.of() : List.copyOf(attachments);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.conversation;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentRun;
|
||||
|
||||
/** Latest durable run fields used to project and order a session-list row. */
|
||||
public record AgentRunListProjection(String status, LocalDateTime gmtUpdate) {
|
||||
|
||||
public static AgentRunListProjection from(AgentRun run) {
|
||||
LocalDateTime update = run.getGmtUpdate();
|
||||
if (update == null) {
|
||||
update = run.getCompletedAt() != null ? run.getCompletedAt()
|
||||
: run.getStartedAt() != null ? run.getStartedAt() : run.getGmtCreate();
|
||||
}
|
||||
return new AgentRunListProjection(run.getStatus(), update);
|
||||
}
|
||||
}
|
||||
+233
@@ -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.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.gateway.conversation;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentRunDao;
|
||||
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentSessionDao;
|
||||
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.entity.agent.AgentRun;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentSession;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Default run ledger service for Agent Gateway.
|
||||
*/
|
||||
@Service
|
||||
public class AgentRunService {
|
||||
|
||||
private final AgentRunDao runDao;
|
||||
private final AgentSessionDao sessionDao;
|
||||
private final EntityManager entityManager;
|
||||
|
||||
public AgentRunService(AgentRunDao runDao, AgentSessionDao sessionDao, EntityManager entityManager) {
|
||||
this.runDao = runDao;
|
||||
this.sessionDao = sessionDao;
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
public AgentRun createOrResumeRun(AgentSession session, UserInput userInput,
|
||||
AgentRuntimeEntryType entryType) {
|
||||
String messageId = userInput.getMessageId();
|
||||
Optional<AgentRun> existingRun = runDao.findBySessionIdAndMessageId(session.getId(), messageId);
|
||||
if (existingRun.isPresent()) {
|
||||
return existingRun.get();
|
||||
}
|
||||
AgentRun run = buildRun(session, userInput, messageId, entryType);
|
||||
try {
|
||||
return runDao.saveAndFlush(run);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
entityManager.clear();
|
||||
return runDao.findBySessionIdAndMessageId(session.getId(), messageId)
|
||||
.orElseThrow(() -> e);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AgentRun markRunning(AgentRun run) {
|
||||
LocalDateTime transitionAt = LocalDateTime.now();
|
||||
run.setStatus(AgentRunStatus.RUNNING.name());
|
||||
run.setStartedAt(transitionAt);
|
||||
run.setCompletedAt(null);
|
||||
run.setErrorMessage(null);
|
||||
AgentRun saved = runDao.save(run);
|
||||
touchSession(run, transitionAt);
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AgentRun markSucceeded(AgentRun run, String resultSummary) {
|
||||
LocalDateTime transitionAt = LocalDateTime.now();
|
||||
run.setStatus(AgentRunStatus.SUCCEEDED.name());
|
||||
run.setResultSummary(GatewayText.redactSecrets(resultSummary));
|
||||
run.setCompletedAt(transitionAt);
|
||||
AgentRun saved = runDao.save(run);
|
||||
touchSession(run, transitionAt);
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AgentRun markFailed(AgentRun run, String errorMessage) {
|
||||
LocalDateTime transitionAt = LocalDateTime.now();
|
||||
run.setStatus(AgentRunStatus.FAILED.name());
|
||||
run.setErrorMessage(GatewayText.safeSummary(errorMessage, 1024));
|
||||
run.setCompletedAt(transitionAt);
|
||||
AgentRun saved = runDao.save(run);
|
||||
touchSession(run, transitionAt);
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AgentRun markCancelled(AgentRun run, String reason) {
|
||||
LocalDateTime transitionAt = LocalDateTime.now();
|
||||
String safeReason = GatewayText.safeSummary(reason, 1024);
|
||||
int updated = runDao.cancelIfActive(run.getId(),
|
||||
List.of(AgentRunStatus.CREATED.name(), AgentRunStatus.RUNNING.name()),
|
||||
AgentRunStatus.CANCELLED.name(), safeReason, transitionAt);
|
||||
if (updated == 1) {
|
||||
touchSession(run, transitionAt);
|
||||
}
|
||||
return runDao.findById(run.getId()).orElseThrow(
|
||||
() -> new IllegalStateException("Agent run disappeared during cancellation"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AgentRun markRecoveryRequired(AgentRun run, String reason) {
|
||||
LocalDateTime transitionAt = LocalDateTime.now();
|
||||
run.setStatus(AgentRunStatus.RECOVERY_REQUIRED.name());
|
||||
run.setErrorMessage(GatewayText.safeSummary(reason, 1024));
|
||||
run.setCompletedAt(transitionAt);
|
||||
AgentRun saved = runDao.save(run);
|
||||
touchSession(run, transitionAt);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Optional<AgentRun> findRun(String runUid) {
|
||||
// External run lookup values may contain surrounding whitespace; normalize before the persistence query.
|
||||
String normalized = GatewayText.normalize(runUid);
|
||||
if (normalized == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return runDao.findByRunUid(normalized);
|
||||
}
|
||||
|
||||
public Optional<AgentRun> findLatestRun(Long sessionId) {
|
||||
return sessionId == null ? Optional.empty() : runDao.findTopBySessionIdOrderByIdDesc(sessionId);
|
||||
}
|
||||
|
||||
public Map<Long, AgentRunListProjection> findLatestRunProjections(Collection<Long> sessionIds) {
|
||||
if (sessionIds == null || sessionIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<Long, AgentRunListProjection> projections = new LinkedHashMap<>();
|
||||
for (AgentRun run : runDao.findLatestBySessionIds(sessionIds)) {
|
||||
if (run.getSessionId() != null && StringUtils.hasText(run.getStatus())) {
|
||||
projections.put(run.getSessionId(), AgentRunListProjection.from(run));
|
||||
}
|
||||
}
|
||||
return Map.copyOf(projections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the complete target snapshot recorded for a run, with a legacy-column fallback.
|
||||
*/
|
||||
public static AgentTargetRef targetFromRun(AgentRun run) {
|
||||
if (run == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.hasText(run.getTargetContextJson())) {
|
||||
AgentTargetRef target = JsonUtil.fromJson(run.getTargetContextJson(), AgentTargetRef.class);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Agent run target context cannot be decoded");
|
||||
}
|
||||
return target;
|
||||
}
|
||||
if (run.getTargetMonitorId() == null
|
||||
&& run.getTargetAlertId() == null
|
||||
&& !StringUtils.hasText(run.getTargetCollector())) {
|
||||
return null;
|
||||
}
|
||||
return AgentTargetRef.builder()
|
||||
.monitorId(run.getTargetMonitorId())
|
||||
.alertId(run.getTargetAlertId())
|
||||
.collector(run.getTargetCollector())
|
||||
.build();
|
||||
}
|
||||
|
||||
public Optional<AgentRun> findCreatedRun(Long sessionId) {
|
||||
return runDao.findFirstBySessionIdAndStatusOrderByGmtCreateAsc(
|
||||
sessionId, AgentRunStatus.CREATED.name());
|
||||
}
|
||||
|
||||
public Optional<AgentRun> findRunningRun(Long sessionId) {
|
||||
return runDao.findFirstBySessionIdAndStatusOrderByGmtCreateAsc(
|
||||
sessionId, AgentRunStatus.RUNNING.name());
|
||||
}
|
||||
|
||||
public boolean hasActiveRun(Long sessionId) {
|
||||
return runDao.existsBySessionIdAndStatusIn(sessionId,
|
||||
List.of(AgentRunStatus.CREATED.name(), AgentRunStatus.RUNNING.name()));
|
||||
}
|
||||
|
||||
private void touchSession(AgentRun run, LocalDateTime transitionAt) {
|
||||
if (sessionDao.advanceGmtUpdate(run.getSessionId(), transitionAt) != 1) {
|
||||
throw new IllegalStateException("Agent run session does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
private AgentRun buildRun(AgentSession session, UserInput userInput, String messageId,
|
||||
AgentRuntimeEntryType entryType) {
|
||||
AgentTargetRef target = userInput.getTarget();
|
||||
return AgentRun.builder()
|
||||
.runUid("run_" + SnowFlakeIdGenerator.generateId())
|
||||
.sessionId(session.getId())
|
||||
.messageId(messageId)
|
||||
.entryType(entryType.name())
|
||||
.targetMonitorId(target == null ? null : target.getMonitorId())
|
||||
.targetAlertId(target == null ? null : target.getAlertId())
|
||||
.targetCollector(target == null ? null : target.getCollector())
|
||||
.targetContextJson(targetJson(target))
|
||||
.status(AgentRunStatus.CREATED.name())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String targetJson(AgentTargetRef target) {
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
String json = JsonUtil.toJson(target);
|
||||
if (!StringUtils.hasText(json)) {
|
||||
throw new IllegalArgumentException("Agent run target context cannot be serialized");
|
||||
}
|
||||
return GatewayText.redactSecrets(json);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.conversation;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
|
||||
|
||||
/**
|
||||
* Owner-checked durable projection of one Agent Gateway run.
|
||||
*/
|
||||
public record AgentRunSnapshot(
|
||||
String runUid,
|
||||
String sessionUid,
|
||||
String messageId,
|
||||
String status,
|
||||
AgentTargetRef target,
|
||||
String result,
|
||||
String errorMessage,
|
||||
boolean replayAvailable,
|
||||
LocalDateTime startedAt,
|
||||
LocalDateTime completedAt,
|
||||
AgentRetryRequest retryRequest) {
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.conversation;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.ai.gateway.application.AgentRunRequestFingerprint;
|
||||
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService;
|
||||
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand;
|
||||
import org.apache.hertzbeat.ai.gateway.channel.core.ChannelId;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.AgentRunRequestSnapshot;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.AgentApprovalHandling;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.AgentGroundingEvidenceVerifier;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.TranscriptMessage;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentRun;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentSession;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Builds the durable result/error projection used by replay and run queries.
|
||||
*/
|
||||
@Service
|
||||
public class AgentRunSnapshotService {
|
||||
|
||||
private static final String LEGACY_COMPLETION_SUMMARY = "Runtime completed.";
|
||||
|
||||
private final AgentTranscriptRecorder transcriptRecorder;
|
||||
private final AgentGroundingEvidenceVerifier groundingVerifier;
|
||||
|
||||
public AgentRunSnapshotService(AgentTranscriptRecorder transcriptRecorder,
|
||||
AgentGroundingEvidenceVerifier groundingVerifier) {
|
||||
this.transcriptRecorder = transcriptRecorder;
|
||||
this.groundingVerifier = groundingVerifier;
|
||||
}
|
||||
|
||||
public AgentRunSnapshot snapshot(AgentSession session, AgentRun run) {
|
||||
AgentRunStatus status = AgentRunStatus.valueOf(run.getStatus());
|
||||
String result = successfulResult(run, status);
|
||||
String error = terminalError(run, status, result);
|
||||
boolean replayAvailable = switch (status) {
|
||||
case SUCCEEDED -> StringUtils.hasText(result);
|
||||
case FAILED, CANCELLED, RECOVERY_REQUIRED -> StringUtils.hasText(error);
|
||||
case CREATED, RUNNING -> true;
|
||||
};
|
||||
return new AgentRunSnapshot(
|
||||
run.getRunUid(),
|
||||
session.getSessionUid(),
|
||||
run.getMessageId(),
|
||||
run.getStatus(),
|
||||
AgentRunService.targetFromRun(run),
|
||||
result,
|
||||
error,
|
||||
replayAvailable,
|
||||
run.getStartedAt(),
|
||||
run.getCompletedAt(),
|
||||
retryRequest(session, run, status));
|
||||
}
|
||||
|
||||
private AgentRetryRequest retryRequest(AgentSession session, AgentRun run, AgentRunStatus status) {
|
||||
if (status == AgentRunStatus.SUCCEEDED || status == AgentRunStatus.RECOVERY_REQUIRED
|
||||
|| !Objects.equals(session.getId(), run.getSessionId())
|
||||
|| !Objects.equals(ChannelId.WEB_UI.id(), session.getChannel())
|
||||
|| !Objects.equals(AgentRuntimeEntryType.USER_INPUT.name(), session.getOriginEntryType())
|
||||
|| !Objects.equals(AgentRuntimeEntryType.USER_INPUT.name(), run.getEntryType())) {
|
||||
return null;
|
||||
}
|
||||
Optional<TranscriptMessage> marker = transcriptRecorder.findUniqueRunRequestMessage(run.getId());
|
||||
if (marker.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
TranscriptMessage message = marker.get();
|
||||
AgentRunRequestSnapshot request = message.getRequestSnapshot();
|
||||
if (message.getRole() != TranscriptMessage.TranscriptRole.USER || message.isPruned() || request == null
|
||||
|| !Objects.equals(AgentRunRequestSnapshot.VERSION, request.version())
|
||||
|| !Objects.equals(AgentRunRequestFingerprint.VERSION, message.getRequestFingerprintVersion())
|
||||
|| !Objects.equals(session.getConversationId(), request.conversationId())
|
||||
|| !Objects.equals(run.getMessageId(), request.messageId())
|
||||
|| !Objects.equals(run.getEntryType(), request.entryType())
|
||||
|| !Objects.equals(AgentRunService.targetFromRun(run), request.target())
|
||||
|| request.alertIncident() != null
|
||||
|| !Objects.equals(message.text(), request.message())
|
||||
|| !Objects.equals(AgentApprovalHandling.WAIT_FOR_DECISION.name(), request.approvalHandling())
|
||||
|| !Objects.equals(GatewayCommand.ReplyMode.STREAM.name(), request.replyMode())
|
||||
|| !StringUtils.hasText(request.message())
|
||||
|| !StringUtils.hasText(request.preferredLanguage())
|
||||
|| request.attachments().stream().anyMatch(value -> !StringUtils.hasText(value))) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (!Objects.equals(message.getRequestFingerprint(), AgentRunRequestFingerprint.from(request))) {
|
||||
return null;
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
return null;
|
||||
}
|
||||
return new AgentRetryRequest(request.conversationId(), request.messageId(), request.message(),
|
||||
AgentTargetCanonicalizationService.retrySourceIntent(request.target()),
|
||||
request.attachments(), request.preferredLanguage());
|
||||
}
|
||||
|
||||
private String terminalError(AgentRun run, AgentRunStatus status, String result) {
|
||||
if (status == AgentRunStatus.SUCCEEDED && !StringUtils.hasText(result)) {
|
||||
return "Agent run result is no longer available.";
|
||||
}
|
||||
if ((status == AgentRunStatus.FAILED || status == AgentRunStatus.CANCELLED
|
||||
|| status == AgentRunStatus.RECOVERY_REQUIRED)
|
||||
&& !StringUtils.hasText(run.getErrorMessage())) {
|
||||
return "Agent run terminal reason is no longer available.";
|
||||
}
|
||||
return status == AgentRunStatus.FAILED || status == AgentRunStatus.CANCELLED
|
||||
|| status == AgentRunStatus.RECOVERY_REQUIRED
|
||||
? run.getErrorMessage() : null;
|
||||
}
|
||||
|
||||
private String successfulResult(AgentRun run, AgentRunStatus status) {
|
||||
if (status != AgentRunStatus.SUCCEEDED) {
|
||||
return null;
|
||||
}
|
||||
if (!groundingVerifier.hasDurableGrounding(run, AgentRunService.targetFromRun(run))) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.hasText(run.getResultSummary())
|
||||
&& !LEGACY_COMPLETION_SUMMARY.equals(run.getResultSummary())) {
|
||||
return run.getResultSummary();
|
||||
}
|
||||
return transcriptRecorder.findRunFinalAssistantMessage(run.getId())
|
||||
.filter(message -> message.toolCalls().isEmpty())
|
||||
.map(TranscriptMessage::text)
|
||||
.filter(StringUtils::hasText)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.conversation;
|
||||
|
||||
/**
|
||||
* Agent run lifecycle states.
|
||||
*/
|
||||
public enum AgentRunStatus {
|
||||
CREATED,
|
||||
RUNNING,
|
||||
SUCCEEDED,
|
||||
FAILED,
|
||||
CANCELLED,
|
||||
RECOVERY_REQUIRED
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.conversation;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
|
||||
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Builds deterministic session keys for Agent Gateway user inputs.
|
||||
*/
|
||||
@Component
|
||||
public class AgentSessionKeyBuilder {
|
||||
|
||||
private static final String KEY_VERSION = "v2:";
|
||||
|
||||
/**
|
||||
* Build a stable session key from transport metadata and user input identity.
|
||||
*/
|
||||
public String build(GatewayEnvelope envelope, String conversationId) {
|
||||
AgentActor actor = envelope.getActor();
|
||||
String canonical = String.join("", List.of(
|
||||
component("channel", envelope.getChannelId()),
|
||||
component("workspace", envelope.getWorkspaceId()),
|
||||
component("actorType", actor.getType()),
|
||||
component("actorId", actor.getId()),
|
||||
component("conversation", conversationId)
|
||||
));
|
||||
return KEY_VERSION + GatewayText.sha256(canonical);
|
||||
}
|
||||
|
||||
private String component(String name, Object source) {
|
||||
// Channel identity fields may contain surrounding whitespace; the session-key boundary canonicalizes them.
|
||||
String normalized = source == null ? null : GatewayText.normalize(String.valueOf(source));
|
||||
String value = normalized == null ? "-" : normalized;
|
||||
return name + "=" + value.length() + ":" + value + ";";
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.conversation;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentSession;
|
||||
|
||||
/**
|
||||
* Session list projection whose status describes the latest durable run rather than session ownership lifecycle.
|
||||
*/
|
||||
public record AgentSessionListItem(
|
||||
Long id,
|
||||
String sessionUid,
|
||||
String conversationId,
|
||||
String status,
|
||||
String title,
|
||||
LocalDateTime gmtCreate,
|
||||
LocalDateTime gmtUpdate) {
|
||||
|
||||
public static AgentSessionListItem from(AgentSession session, AgentRunListProjection latestRun) {
|
||||
String projectedStatus = latestRun == null ? "NO_RUN" : latestRun.status();
|
||||
LocalDateTime projectedUpdate = session.getGmtUpdate();
|
||||
if (latestRun != null && latestRun.gmtUpdate() != null
|
||||
&& (projectedUpdate == null || latestRun.gmtUpdate().isAfter(projectedUpdate))) {
|
||||
projectedUpdate = latestRun.gmtUpdate();
|
||||
}
|
||||
return new AgentSessionListItem(
|
||||
session.getId(),
|
||||
session.getSessionUid(),
|
||||
session.getConversationId(),
|
||||
projectedStatus,
|
||||
session.getTitle(),
|
||||
session.getGmtCreate(),
|
||||
projectedUpdate);
|
||||
}
|
||||
}
|
||||
+392
@@ -0,0 +1,392 @@
|
||||
/*
|
||||
* 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.conversation;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentSessionDao;
|
||||
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentTranscriptEntryDao;
|
||||
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.UserInput.Message;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeHistoryWindow;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.TranscriptMessage;
|
||||
import org.apache.hertzbeat.ai.gateway.identity.ActorSupport;
|
||||
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentSession;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentSessionStatus;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentTranscriptEntry;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Default session and transcript service for Agent Gateway.
|
||||
*/
|
||||
@Service
|
||||
public class AgentSessionService {
|
||||
|
||||
private static final int TRANSCRIPT_QUERY_PAGE_SIZE = 100;
|
||||
private static final int TRANSCRIPT_ROLE_LIMIT = 32;
|
||||
|
||||
private final AgentSessionDao sessionDao;
|
||||
private final AgentTranscriptEntryDao transcriptEntryDao;
|
||||
private final AgentSessionKeyBuilder sessionKeyBuilder;
|
||||
private final EntityManager entityManager;
|
||||
|
||||
public AgentSessionService(AgentSessionDao sessionDao, AgentTranscriptEntryDao transcriptEntryDao,
|
||||
AgentSessionKeyBuilder sessionKeyBuilder, EntityManager entityManager) {
|
||||
this.sessionDao = sessionDao;
|
||||
this.transcriptEntryDao = transcriptEntryDao;
|
||||
this.sessionKeyBuilder = sessionKeyBuilder;
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
public AgentSession findOrCreateSession(GatewayEnvelope envelope, UserInput userInput,
|
||||
AgentRuntimeEntryType originEntryType) {
|
||||
AgentActor actor = envelope.getActor();
|
||||
String sessionKey = sessionKeyBuilder.build(envelope, userInput.getConversationId());
|
||||
Optional<AgentSession> existed = sessionDao.findBySessionKey(sessionKey);
|
||||
if (existed.isPresent()) {
|
||||
return existed.get();
|
||||
}
|
||||
Message message = userInput.getMessage();
|
||||
AgentSession session = AgentSession.builder()
|
||||
.sessionUid("ags_" + SnowFlakeIdGenerator.generateId())
|
||||
.sessionKey(sessionKey)
|
||||
.workspaceId(envelope.getWorkspaceId())
|
||||
.channel(envelope.getChannelId())
|
||||
.originEntryType(originEntryType.name())
|
||||
.conversationId(userInput.getConversationId())
|
||||
.actorType(actor.getType())
|
||||
.actorId(actor.getId())
|
||||
.actorRoles(ActorSupport.rolesJson(actor))
|
||||
.status(AgentSessionStatus.ACTIVE)
|
||||
.title(GatewayText.safeSummary(message.getText(), 128))
|
||||
.build();
|
||||
try {
|
||||
return sessionDao.saveAndFlush(session);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
entityManager.clear();
|
||||
return sessionDao.findBySessionKey(sessionKey).orElseThrow(() -> e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Read-only session probe used before target canonicalization and admission locking. */
|
||||
public Optional<AgentSession> findSession(GatewayEnvelope envelope, String conversationId) {
|
||||
return sessionDao.findBySessionKey(sessionKeyBuilder.build(envelope, conversationId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AgentTranscriptEntry recordTranscriptEntry(AgentTranscriptEntry entry) {
|
||||
if (entry == null) {
|
||||
throw new IllegalArgumentException("Transcript entry must not be null");
|
||||
}
|
||||
if (entry.getSessionId() == null) {
|
||||
throw new IllegalArgumentException("Transcript session id must not be null");
|
||||
}
|
||||
String rawPayload = entry.getPayloadJson();
|
||||
if (!StringUtils.hasText(rawPayload)) {
|
||||
throw new IllegalArgumentException("Transcript payload JSON must not be blank");
|
||||
}
|
||||
// The role is the persisted discriminator used by checkpoint queries and cannot be inferred from SQL.
|
||||
if (!StringUtils.hasText(entry.getMessageRole())) {
|
||||
throw new IllegalArgumentException("Transcript message role must not be blank");
|
||||
}
|
||||
entry.setSessionSequence(nextSessionSequence(entry.getSessionId()));
|
||||
entry.setPayloadJson(GatewayText.redactSecrets(rawPayload));
|
||||
entry.setMessageRole(GatewayText.requireBounded(
|
||||
entry.getMessageRole(), TRANSCRIPT_ROLE_LIMIT, "Transcript message role"));
|
||||
return transcriptEntryDao.save(entry);
|
||||
}
|
||||
|
||||
public Optional<AgentSession> findSession(String sessionId) {
|
||||
// External session lookup values may contain surrounding whitespace; normalize before numeric/UID lookup.
|
||||
String normalized = GatewayText.normalize(sessionId);
|
||||
if (normalized == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (normalized.chars().allMatch(Character::isDigit)) {
|
||||
return sessionDao.findById(Long.parseLong(normalized));
|
||||
}
|
||||
return sessionDao.findBySessionUid(normalized);
|
||||
}
|
||||
|
||||
public Optional<AgentSession> findOwnedSession(
|
||||
String sessionId, GatewayEnvelope envelope, AgentRuntimeEntryType originEntryType) {
|
||||
AgentActor actor = envelope.getActor();
|
||||
if (!ActorSupport.hasIdentity(actor)) {
|
||||
throw new IllegalArgumentException("Session query actor is required");
|
||||
}
|
||||
// External session lookup values may contain surrounding whitespace; normalize before owner-scoped lookup.
|
||||
String normalized = GatewayText.normalize(sessionId);
|
||||
if (normalized == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (normalized.chars().allMatch(Character::isDigit)) {
|
||||
return sessionDao.findByIdAndWorkspaceIdAndChannelAndActorTypeAndActorIdAndOriginEntryType(
|
||||
Long.parseLong(normalized), envelope.getWorkspaceId(), envelope.getChannelId(), actor.getType(),
|
||||
actor.getId(), originEntryType.name());
|
||||
}
|
||||
return sessionDao.findBySessionUidAndWorkspaceIdAndChannelAndActorTypeAndActorIdAndOriginEntryType(
|
||||
normalized, envelope.getWorkspaceId(), envelope.getChannelId(), actor.getType(), actor.getId(),
|
||||
originEntryType.name());
|
||||
}
|
||||
|
||||
public Page<AgentSession> findSessions(
|
||||
GatewayEnvelope envelope, AgentRuntimeEntryType originEntryType, String title, Pageable pageable) {
|
||||
AgentActor actor = envelope.getActor();
|
||||
if (!ActorSupport.hasIdentity(actor)) {
|
||||
throw new IllegalArgumentException("Session query actor is required");
|
||||
}
|
||||
if (!StringUtils.hasText(title)) {
|
||||
return sessionDao.findByWorkspaceIdAndChannelAndActorTypeAndActorIdAndOriginEntryTypeOrderByGmtUpdateDesc(
|
||||
envelope.getWorkspaceId(), envelope.getChannelId(), actor.getType(), actor.getId(),
|
||||
originEntryType.name(), pageable);
|
||||
}
|
||||
return sessionDao
|
||||
.findByWorkspaceIdAndChannelAndActorTypeAndActorIdAndOriginEntryTypeAndTitleContainingIgnoreCaseOrderByGmtUpdateDesc(
|
||||
envelope.getWorkspaceId(), envelope.getChannelId(), actor.getType(), actor.getId(),
|
||||
originEntryType.name(), title, pageable);
|
||||
}
|
||||
|
||||
public Page<AgentTranscriptEntry> findTranscriptEntries(Long sessionId, Pageable pageable) {
|
||||
return transcriptEntryDao.findBySessionIdOrderBySessionSequenceAsc(sessionId, pageable);
|
||||
}
|
||||
|
||||
public Page<AgentTranscriptEntry> findConversationTranscriptEntries(Long sessionId, Pageable pageable) {
|
||||
return transcriptEntryDao.findBySessionIdAndMessageRoleInOrderBySessionSequenceDesc(
|
||||
sessionId,
|
||||
List.of(TranscriptMessage.TranscriptRole.USER.wireValue(),
|
||||
TranscriptMessage.TranscriptRole.ASSISTANT.wireValue()),
|
||||
pageable);
|
||||
}
|
||||
|
||||
public Optional<TranscriptMessage> findFirstRunTranscriptMessage(Long runId, TranscriptMessage.TranscriptRole role) {
|
||||
if (runId == null || role == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return transcriptEntryDao.findFirstByRunIdAndMessageRoleOrderBySessionSequenceAsc(runId, role.wireValue())
|
||||
.map(this::transcriptMessage)
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
public Optional<TranscriptMessage> findUniqueRunTranscriptMessage(
|
||||
Long runId, TranscriptMessage.TranscriptRole role) {
|
||||
if (runId == null || role == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
List<AgentTranscriptEntry> entries = transcriptEntryDao
|
||||
.findTop2ByRunIdAndMessageRoleOrderBySessionSequenceAsc(runId, role.wireValue());
|
||||
if (entries.size() != 1) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(transcriptMessage(entries.getFirst()));
|
||||
}
|
||||
|
||||
public Optional<TranscriptMessage> findLatestRunTranscriptMessage(Long runId,
|
||||
TranscriptMessage.TranscriptRole role) {
|
||||
if (runId == null || role == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return transcriptEntryDao.findTopByRunIdAndMessageRoleOrderBySessionSequenceDesc(runId, role.wireValue())
|
||||
.map(this::transcriptMessage)
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
public List<TranscriptMessage> findRunTranscriptMessages(Long runId, TranscriptMessage.TranscriptRole role) {
|
||||
if (runId == null || role == null) {
|
||||
return List.of();
|
||||
}
|
||||
return transcriptEntryDao.findByRunIdAndMessageRoleOrderBySessionSequenceAsc(runId, role.wireValue())
|
||||
.stream()
|
||||
.map(this::transcriptMessage)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<TranscriptMessage> findRecentTranscriptMessages(Long sessionId) {
|
||||
if (sessionId == null) {
|
||||
return List.of();
|
||||
}
|
||||
Optional<TranscriptCheckpointSource> latestCheckpoint = latestCompactionCheckpoint(sessionId);
|
||||
List<TranscriptMessage> selected = latestCheckpoint
|
||||
.map(checkpoint -> transcriptMessagesSinceCheckpoint(sessionId, checkpoint))
|
||||
.orElseGet(() -> recentTranscriptMessages(sessionId));
|
||||
if (selected.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return AgentRuntimeHistoryWindow.replayWindow(selected);
|
||||
}
|
||||
|
||||
private List<TranscriptMessage> recentTranscriptMessages(Long sessionId) {
|
||||
List<AgentTranscriptEntry> recentEntries = recentTranscriptEntries(sessionId);
|
||||
if (recentEntries.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Collections.reverse(recentEntries);
|
||||
return transcriptMessages(recentEntries, false);
|
||||
}
|
||||
|
||||
private Optional<TranscriptCheckpointSource> latestCompactionCheckpoint(Long sessionId) {
|
||||
return transcriptEntryDao.findTopBySessionIdAndMessageRoleOrderBySessionSequenceDesc(
|
||||
sessionId, TranscriptMessage.TranscriptRole.COMPACTION_SUMMARY.wireValue())
|
||||
.flatMap(entry -> {
|
||||
TranscriptMessage message = transcriptMessage(entry);
|
||||
if (message == null
|
||||
|| message.getRole() != TranscriptMessage.TranscriptRole.COMPACTION_SUMMARY
|
||||
|| !message.hasReplayContent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new TranscriptCheckpointSource(entry, message));
|
||||
});
|
||||
}
|
||||
|
||||
private List<TranscriptMessage> transcriptMessagesSinceCheckpoint(Long sessionId,
|
||||
TranscriptCheckpointSource checkpoint) {
|
||||
List<TranscriptMessage> messages = new ArrayList<>();
|
||||
messages.add(checkpoint.message());
|
||||
Long firstKeptSessionSequence = checkpoint.message().compactionFirstKeptSessionSequence();
|
||||
if (firstKeptSessionSequence == null) {
|
||||
Long checkpointSessionSequence = checkpoint.entry().getSessionSequence();
|
||||
firstKeptSessionSequence = checkpointSessionSequence == null ? 1L : checkpointSessionSequence + 1L;
|
||||
}
|
||||
messages.addAll(transcriptMessages(transcriptEntriesFrom(sessionId, firstKeptSessionSequence), true));
|
||||
return messages;
|
||||
}
|
||||
|
||||
private List<AgentTranscriptEntry> recentTranscriptEntries(Long sessionId) {
|
||||
List<AgentTranscriptEntry> entries = new ArrayList<>();
|
||||
for (int page = 0; ; page++) {
|
||||
List<AgentTranscriptEntry> pageEntries =
|
||||
transcriptEntryDao.findBySessionIdOrderBySessionSequenceDesc(
|
||||
sessionId, PageRequest.of(page, TRANSCRIPT_QUERY_PAGE_SIZE));
|
||||
if (pageEntries.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
entries.addAll(pageEntries);
|
||||
if (pageEntries.size() < TRANSCRIPT_QUERY_PAGE_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private List<AgentTranscriptEntry> transcriptEntriesFrom(Long sessionId, Long firstSessionSequence) {
|
||||
List<AgentTranscriptEntry> entries = new ArrayList<>();
|
||||
Long start = firstSessionSequence == null ? 1L : Math.max(1L, firstSessionSequence);
|
||||
for (int page = 0; ; page++) {
|
||||
List<AgentTranscriptEntry> pageEntries =
|
||||
transcriptEntryDao
|
||||
.findBySessionIdAndSessionSequenceGreaterThanEqualOrderBySessionSequenceAsc(
|
||||
sessionId, start,
|
||||
PageRequest.of(page, TRANSCRIPT_QUERY_PAGE_SIZE));
|
||||
if (pageEntries.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
entries.addAll(pageEntries);
|
||||
if (pageEntries.size() < TRANSCRIPT_QUERY_PAGE_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private List<TranscriptMessage> transcriptMessages(List<AgentTranscriptEntry> entries,
|
||||
boolean skipCompactionSummaries) {
|
||||
List<TranscriptMessage> messages = new ArrayList<>(entries.size());
|
||||
for (AgentTranscriptEntry entry : entries) {
|
||||
TranscriptMessage message = transcriptMessage(entry);
|
||||
if (skipCompactionSummaries
|
||||
&& message != null
|
||||
&& message.getRole() == TranscriptMessage.TranscriptRole.COMPACTION_SUMMARY) {
|
||||
continue;
|
||||
}
|
||||
if (!isEmptyTranscriptMessage(message)) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void persistCompactionCheckpoint(Long sessionId,
|
||||
AgentRuntimeHistoryWindow.CompactionCheckpoint checkpoint) {
|
||||
if (checkpoint == null || checkpoint.message() == null
|
||||
|| checkpoint.summarizedThroughSessionSequence() == null
|
||||
|| checkpoint.firstKeptSessionSequence() == null) {
|
||||
return;
|
||||
}
|
||||
Long latestSummarizedThrough = latestCompactionCheckpoint(sessionId)
|
||||
.map(TranscriptCheckpointSource::message)
|
||||
.map(TranscriptMessage::compactionSummarizedThroughSessionSequence)
|
||||
.orElse(null);
|
||||
if (latestSummarizedThrough != null
|
||||
&& checkpoint.summarizedThroughSessionSequence() <= latestSummarizedThrough) {
|
||||
return;
|
||||
}
|
||||
recordTranscriptEntry(AgentTranscriptEntry.builder()
|
||||
.sessionId(sessionId)
|
||||
.payloadJson(JsonUtil.toJson(checkpoint.message()))
|
||||
.messageRole(TranscriptMessage.TranscriptRole.COMPACTION_SUMMARY.wireValue())
|
||||
.build());
|
||||
}
|
||||
|
||||
private long nextSessionSequence(Long sessionId) {
|
||||
AgentSession session = sessionDao.findFirstById(sessionId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Transcript session does not exist: " + sessionId));
|
||||
long nextSequence = Optional.ofNullable(session.getTranscriptSequence()).orElse(0L) + 1L;
|
||||
session.setTranscriptSequence(nextSequence);
|
||||
return nextSequence;
|
||||
}
|
||||
|
||||
private TranscriptMessage transcriptMessage(AgentTranscriptEntry entry) {
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
TranscriptMessage message = JsonUtil.fromJsonQuietly(entry.getPayloadJson(), TranscriptMessage.class);
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
return message.toBuilder()
|
||||
.sessionSequence(entry.getSessionSequence())
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean isEmptyTranscriptMessage(TranscriptMessage message) {
|
||||
return message == null
|
||||
|| message.getRole() == null && !message.hasReplayContent();
|
||||
}
|
||||
|
||||
private record TranscriptCheckpointSource(AgentTranscriptEntry entry, TranscriptMessage message) {
|
||||
}
|
||||
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
* 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.conversation;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.AgentAlertIncidentContext;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.AgentLogRef;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.AgentRunRequestSnapshot;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.AgentSignalRef;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.AgentServiceRef;
|
||||
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.apache.hertzbeat.ai.gateway.contract.AgentTraceRef;
|
||||
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeHistoryWindow;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeTextSanitizer;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.TranscriptContent;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.TranscriptMessage;
|
||||
import org.apache.hertzbeat.ai.gateway.text.GatewaySecretRedactor;
|
||||
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.apache.hertzbeat.common.entity.agent.AgentTranscriptEntry;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Records and rehydrates Agent Gateway transcript messages.
|
||||
*/
|
||||
@Service
|
||||
public class AgentTranscriptRecorder {
|
||||
|
||||
private static final int TRANSCRIPT_TOOL_NAME_LIMIT = 128;
|
||||
private static final int TRANSCRIPT_TOOL_CALL_ID_LIMIT = 128;
|
||||
private static final int TRANSCRIPT_TOOL_ERROR_LIMIT = 2048;
|
||||
private static final int MYSQL_TEXT_MAX_UTF8_BYTES = 65535;
|
||||
private static final String REDACTED_REQUEST_VALUE = "[REDACTED]";
|
||||
|
||||
private final AgentSessionService sessionService;
|
||||
|
||||
public AgentTranscriptRecorder(AgentSessionService sessionService) {
|
||||
// Transcript persistence is required for every recorder operation.
|
||||
this.sessionService = Objects.requireNonNull(sessionService, "sessionService must not be null");
|
||||
}
|
||||
|
||||
public List<TranscriptMessage> chatHistory(Long sessionId) {
|
||||
return List.copyOf(sessionService.findRecentTranscriptMessages(sessionId));
|
||||
}
|
||||
|
||||
public void recordCompactionCheckpoint(AgentSession session,
|
||||
AgentRuntimeHistoryWindow.CompactionCheckpoint checkpoint) {
|
||||
sessionService.persistCompactionCheckpoint(session.getId(), checkpoint);
|
||||
}
|
||||
|
||||
public AgentTranscriptEntry recordUserTranscriptEntry(AgentSession session, AgentRun run, UserInput userInput) {
|
||||
return recordUserTranscriptEntry(session, run, userInput, null, null);
|
||||
}
|
||||
|
||||
public AgentTranscriptEntry recordUserTranscriptEntry(AgentSession session, AgentRun run, UserInput userInput,
|
||||
String fingerprintVersion, String fingerprint) {
|
||||
return recordUserTranscriptEntry(session, run, userInput, fingerprintVersion, fingerprint, null);
|
||||
}
|
||||
|
||||
public AgentTranscriptEntry recordUserTranscriptEntry(AgentSession session, AgentRun run, UserInput userInput,
|
||||
String fingerprintVersion, String fingerprint,
|
||||
AgentRunRequestSnapshot requestSnapshot) {
|
||||
TranscriptMessage message = validateTranscriptMessage(TranscriptMessage.userText(
|
||||
userInput.getMessage().getText(), fingerprintVersion, fingerprint,
|
||||
safeRequestSnapshot(requestSnapshot)));
|
||||
if (message.getRequestSnapshot() != null && exceedsTranscriptPayloadBudget(message)) {
|
||||
message = message.toBuilder().requestSnapshot(null).build();
|
||||
}
|
||||
return persistTranscriptMessage(session, run, message);
|
||||
}
|
||||
|
||||
public Optional<TranscriptMessage> findRunRequestMessage(Long runId) {
|
||||
return sessionService.findFirstRunTranscriptMessage(runId, TranscriptMessage.TranscriptRole.USER);
|
||||
}
|
||||
|
||||
public Optional<TranscriptMessage> findUniqueRunRequestMessage(Long runId) {
|
||||
return sessionService.findUniqueRunTranscriptMessage(runId, TranscriptMessage.TranscriptRole.USER);
|
||||
}
|
||||
|
||||
public Optional<TranscriptMessage> findRunFinalAssistantMessage(Long runId) {
|
||||
return sessionService.findLatestRunTranscriptMessage(runId, TranscriptMessage.TranscriptRole.ASSISTANT);
|
||||
}
|
||||
|
||||
public List<TranscriptMessage> findRunGroundingMessages(Long runId) {
|
||||
return sessionService.findRunTranscriptMessages(runId, TranscriptMessage.TranscriptRole.TOOL_RESULT);
|
||||
}
|
||||
|
||||
/** Append a runtime message immediately using the owning session's durable sequence. */
|
||||
public AgentTranscriptEntry recordRunMessage(AgentSession session, AgentRun run, TranscriptMessage message) {
|
||||
return recordTranscriptMessage(session, run, message);
|
||||
}
|
||||
|
||||
private AgentTranscriptEntry recordTranscriptMessage(AgentSession session, AgentRun run,
|
||||
TranscriptMessage message) {
|
||||
return persistTranscriptMessage(session, run, validateTranscriptMessage(message));
|
||||
}
|
||||
|
||||
private AgentTranscriptEntry persistTranscriptMessage(AgentSession session, AgentRun run,
|
||||
TranscriptMessage validatedMessage) {
|
||||
return sessionService.recordTranscriptEntry(AgentTranscriptEntry.builder()
|
||||
.sessionId(session.getId())
|
||||
.runId(run.getId())
|
||||
.payloadJson(toJson(validatedMessage))
|
||||
.messageRole(validatedMessage.getRole().wireValue())
|
||||
.build());
|
||||
}
|
||||
|
||||
private AgentRunRequestSnapshot safeRequestSnapshot(AgentRunRequestSnapshot request) {
|
||||
try {
|
||||
return validateRequestSnapshot(request);
|
||||
} catch (RuntimeException ignored) {
|
||||
// Recovery metadata is optional and must never prevent the authoritative USER entry.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean exceedsTranscriptPayloadBudget(TranscriptMessage message) {
|
||||
String finalPayload = GatewayText.redactSecrets(toJson(message));
|
||||
return finalPayload.getBytes(StandardCharsets.UTF_8).length > MYSQL_TEXT_MAX_UTF8_BYTES;
|
||||
}
|
||||
|
||||
private TranscriptMessage validateTranscriptMessage(TranscriptMessage message) {
|
||||
// Every persisted payload must have a supported role so checkpoint queries and replay remain deterministic.
|
||||
if (message.getRole() == null) {
|
||||
throw new IllegalArgumentException("Transcript message role must not be null");
|
||||
}
|
||||
return message.toBuilder()
|
||||
.toolCallId(GatewayText.requireBounded(
|
||||
message.getToolCallId(), TRANSCRIPT_TOOL_CALL_ID_LIMIT,
|
||||
"Transcript model tool-call id"))
|
||||
.toolName(GatewayText.requireBounded(
|
||||
message.getToolName(), TRANSCRIPT_TOOL_NAME_LIMIT, "Transcript tool name"))
|
||||
.errorMessage(GatewayText.requireBounded(
|
||||
AgentRuntimeTextSanitizer.sanitizeAndLimit(
|
||||
message.getErrorMessage(), TRANSCRIPT_TOOL_ERROR_LIMIT),
|
||||
TRANSCRIPT_TOOL_ERROR_LIMIT,
|
||||
"Transcript tool error"))
|
||||
.content(validateTranscriptContent(message.getContent()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private AgentRunRequestSnapshot validateRequestSnapshot(AgentRunRequestSnapshot request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
List<String> attachments = request.attachments().stream()
|
||||
.map(value -> GatewayText.requireBounded(value, 8192, "Agent request attachment"))
|
||||
.toList();
|
||||
AgentRunRequestSnapshot bounded = request.toBuilder()
|
||||
.version(GatewayText.requireBounded(request.version(), 16, "Agent request snapshot version"))
|
||||
.conversationId(GatewayText.requireBounded(
|
||||
request.conversationId(), 256, "Agent request conversation id"))
|
||||
.messageId(GatewayText.requireBounded(request.messageId(), 128, "Agent request message id"))
|
||||
.entryType(GatewayText.requireBounded(request.entryType(), 32, "Agent request entry type"))
|
||||
.message(GatewayText.requireBounded(request.message(), 8192, "Agent request message"))
|
||||
.preferredLanguage(GatewayText.requireBounded(
|
||||
request.preferredLanguage(), 128, "Agent request preferred language"))
|
||||
.approvalHandling(GatewayText.requireBounded(
|
||||
request.approvalHandling(), 32, "Agent request approval handling"))
|
||||
.replyMode(GatewayText.requireBounded(request.replyMode(), 32, "Agent request reply mode"))
|
||||
.attachments(attachments)
|
||||
.build();
|
||||
return bounded.toBuilder()
|
||||
.conversationId(redactRequestValue(bounded.conversationId()))
|
||||
.messageId(redactRequestValue(bounded.messageId()))
|
||||
.entryType(redactRequestValue(bounded.entryType()))
|
||||
.target(redactTarget(bounded.target()))
|
||||
.alertIncident(redactIncident(bounded.alertIncident()))
|
||||
.message(redactRequestValue(bounded.message()))
|
||||
.attachments(bounded.attachments().stream().map(this::redactRequestValue).toList())
|
||||
.preferredLanguage(redactRequestValue(bounded.preferredLanguage()))
|
||||
.approvalHandling(redactRequestValue(bounded.approvalHandling()))
|
||||
.replyMode(redactRequestValue(bounded.replyMode()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private AgentAlertIncidentContext redactIncident(AgentAlertIncidentContext incident) {
|
||||
if (incident == null) {
|
||||
return null;
|
||||
}
|
||||
String json = JsonUtil.toJson(incident);
|
||||
return Objects.equals(json, GatewayText.redactSecrets(json)) ? incident : null;
|
||||
}
|
||||
|
||||
private AgentTargetRef redactTarget(AgentTargetRef target) {
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
AgentSignalRef signal = target.getSignal();
|
||||
AgentTopologyRef topology = target.getTopology();
|
||||
AgentTraceRef trace = target.getTrace();
|
||||
AgentLogRef log = target.getLog();
|
||||
AgentServiceRef service = target.getService();
|
||||
AgentTargetAuthority authority = target.getAuthority();
|
||||
return AgentTargetRef.builder()
|
||||
.version(redactRequestValue(target.getVersion()))
|
||||
.monitorId(target.getMonitorId())
|
||||
.alertId(target.getAlertId())
|
||||
.entityId(target.getEntityId())
|
||||
.collector(redactRequestValue(target.getCollector()))
|
||||
.signal(signal == null ? null : AgentSignalRef.builder()
|
||||
.type(redactRequestValue(signal.getType()))
|
||||
.query(redactRequestValue(signal.getQuery()))
|
||||
.timeRange(redactRequestValue(signal.getTimeRange()))
|
||||
.start(signal.getStart())
|
||||
.end(signal.getEnd())
|
||||
.timezone(redactRequestValue(signal.getTimezone()))
|
||||
.build())
|
||||
.topology(topology == null ? null : AgentTopologyRef.builder()
|
||||
.rootEntityId(topology.getRootEntityId())
|
||||
.nodeId(redactRequestValue(topology.getNodeId()))
|
||||
.edgeId(redactRequestValue(topology.getEdgeId()))
|
||||
.depth(topology.getDepth())
|
||||
.environment(redactRequestValue(topology.getEnvironment()))
|
||||
.sourceKind(redactRequestValue(topology.getSourceKind()))
|
||||
.start(topology.getStart())
|
||||
.end(topology.getEnd())
|
||||
.relationType(redactRequestValue(topology.getRelationType()))
|
||||
.hideInternal(topology.getHideInternal())
|
||||
.pageIndex(topology.getPageIndex())
|
||||
.pageSize(topology.getPageSize())
|
||||
.build())
|
||||
.trace(trace == null ? null : AgentTraceRef.builder()
|
||||
.traceId(redactRequestValue(trace.getTraceId()))
|
||||
.spanId(redactRequestValue(trace.getSpanId()))
|
||||
.start(trace.getStart())
|
||||
.end(trace.getEnd())
|
||||
.serviceName(redactRequestValue(trace.getServiceName()))
|
||||
.serviceNamespace(redactRequestValue(trace.getServiceNamespace()))
|
||||
.environment(redactRequestValue(trace.getEnvironment()))
|
||||
.resourceFilter(redactRequestValue(trace.getResourceFilter()))
|
||||
.attributeFilter(redactRequestValue(trace.getAttributeFilter()))
|
||||
.minDurationMs(trace.getMinDurationMs())
|
||||
.maxDurationMs(trace.getMaxDurationMs())
|
||||
.build())
|
||||
.log(log == null ? null : AgentLogRef.builder()
|
||||
.start(log.getStart())
|
||||
.end(log.getEnd())
|
||||
.traceId(redactRequestValue(log.getTraceId()))
|
||||
.spanId(redactRequestValue(log.getSpanId()))
|
||||
.severityNumber(log.getSeverityNumber())
|
||||
.severityText(redactRequestValue(log.getSeverityText()))
|
||||
.search(redactRequestValue(log.getSearch()))
|
||||
.serviceName(redactRequestValue(log.getServiceName()))
|
||||
.serviceNamespace(redactRequestValue(log.getServiceNamespace()))
|
||||
.environment(redactRequestValue(log.getEnvironment()))
|
||||
.resourceFilter(redactRequestValue(log.getResourceFilter()))
|
||||
.attributeFilter(redactRequestValue(log.getAttributeFilter()))
|
||||
.hideInternal(log.getHideInternal())
|
||||
.hideNoise(log.getHideNoise())
|
||||
.pageIndex(log.getPageIndex())
|
||||
.pageSize(log.getPageSize())
|
||||
.build())
|
||||
.service(service == null ? null : AgentServiceRef.builder()
|
||||
.name(redactRequestValue(service.getName()))
|
||||
.namespace(redactRequestValue(service.getNamespace()))
|
||||
.environment(redactRequestValue(service.getEnvironment()))
|
||||
.build())
|
||||
.authority(authority == null ? null : AgentTargetAuthority.builder()
|
||||
.bindingId(authority.getBindingId())
|
||||
.version(redactRequestValue(authority.getVersion()))
|
||||
.hash(redactRequestValue(authority.getHash()))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private String redactRequestValue(String value) {
|
||||
String redacted = GatewayText.redactSecrets(value);
|
||||
return Objects.equals(value, redacted) ? value : REDACTED_REQUEST_VALUE;
|
||||
}
|
||||
|
||||
private List<TranscriptContent> validateTranscriptContent(List<TranscriptContent> content) {
|
||||
if (content == null || content.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return content.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(block -> block.toBuilder()
|
||||
.id(GatewayText.requireBounded(
|
||||
block.getId(), TRANSCRIPT_TOOL_CALL_ID_LIMIT, "Transcript tool-call id"))
|
||||
.name(GatewayText.requireBounded(
|
||||
block.getName(), TRANSCRIPT_TOOL_NAME_LIMIT, "Transcript tool name"))
|
||||
.text(GatewayText.redactSecrets(block.getText()))
|
||||
.input(GatewaySecretRedactor.redactMap(
|
||||
block.getInput() == null ? Map.of() : block.getInput()))
|
||||
.build())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String toJson(TranscriptMessage message) {
|
||||
String json = JsonUtil.toJson(message);
|
||||
if (GatewayText.isBlank(json)) {
|
||||
throw new IllegalArgumentException("Agent transcript payload cannot be serialized");
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.conversation;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentTranscriptEntryDao;
|
||||
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Deletes durable Agent Gateway transcript entries after their configured retention period.
|
||||
*/
|
||||
@Service
|
||||
public class AgentTranscriptRetentionService {
|
||||
|
||||
private final AgentTranscriptEntryDao transcriptEntryDao;
|
||||
private final AgentRuntimeProperties properties;
|
||||
private final Clock clock;
|
||||
|
||||
@Autowired
|
||||
public AgentTranscriptRetentionService(AgentTranscriptEntryDao transcriptEntryDao,
|
||||
AgentRuntimeProperties properties) {
|
||||
this(transcriptEntryDao, properties, Clock.systemUTC());
|
||||
}
|
||||
|
||||
AgentTranscriptRetentionService(AgentTranscriptEntryDao transcriptEntryDao,
|
||||
AgentRuntimeProperties properties,
|
||||
Clock clock) {
|
||||
this.transcriptEntryDao = Objects.requireNonNull(
|
||||
transcriptEntryDao, "transcriptEntryDao must not be null");
|
||||
this.properties = Objects.requireNonNull(properties, "properties must not be null");
|
||||
this.clock = Objects.requireNonNull(clock, "clock must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge transcript entries whose creation timestamp is older than the retention boundary.
|
||||
*
|
||||
* @return number of deleted transcript entries
|
||||
*/
|
||||
@Scheduled(cron = "${hertzbeat.agent.runtime.transcript-retention-cron:0 30 2 * * *}")
|
||||
@Transactional
|
||||
public long purgeExpiredTranscripts() {
|
||||
LocalDateTime cutoff = LocalDateTime.ofInstant(
|
||||
clock.instant().minus(properties.getTranscriptRetention()), clock.getZone());
|
||||
return transcriptEntryDao.deleteByGmtCreateBefore(cutoff);
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.conversation.persistence;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentRun;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Repository for Agent Gateway runs.
|
||||
*/
|
||||
@Repository
|
||||
public interface AgentRunDao extends JpaRepository<AgentRun, Long> {
|
||||
|
||||
/**
|
||||
* Find a run by public run UID.
|
||||
*/
|
||||
Optional<AgentRun> findByRunUid(String runUid);
|
||||
|
||||
/**
|
||||
* Find the existing run for a session event.
|
||||
*/
|
||||
Optional<AgentRun> findBySessionIdAndMessageId(Long sessionId, String messageId);
|
||||
|
||||
Optional<AgentRun> findTopBySessionIdOrderByIdDesc(Long sessionId);
|
||||
|
||||
@Query("""
|
||||
select run from AgentRun run
|
||||
where run.id in (
|
||||
select max(candidate.id) from AgentRun candidate
|
||||
where candidate.sessionId in :sessionIds
|
||||
group by candidate.sessionId
|
||||
)
|
||||
""")
|
||||
List<AgentRun> findLatestBySessionIds(@Param("sessionIds") Collection<Long> sessionIds);
|
||||
|
||||
Optional<AgentRun> findFirstBySessionIdAndStatusOrderByGmtCreateAsc(Long sessionId, String status);
|
||||
|
||||
boolean existsBySessionIdAndStatusIn(Long sessionId, List<String> statuses);
|
||||
|
||||
/** Cancel only a run that has not already reached a truthful terminal state. */
|
||||
@Modifying(clearAutomatically = true, flushAutomatically = true)
|
||||
@Query("""
|
||||
update AgentRun run
|
||||
set run.status = :cancelledStatus,
|
||||
run.errorMessage = :reason,
|
||||
run.completedAt = :transitionAt,
|
||||
run.gmtUpdate = :transitionAt
|
||||
where run.id = :runId and run.status in :activeStatuses
|
||||
""")
|
||||
int cancelIfActive(@Param("runId") Long runId,
|
||||
@Param("activeStatuses") Collection<String> activeStatuses,
|
||||
@Param("cancelledStatus") String cancelledStatus,
|
||||
@Param("reason") String reason,
|
||||
@Param("transitionAt") java.time.LocalDateTime transitionAt);
|
||||
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.conversation.persistence;
|
||||
|
||||
import jakarta.persistence.LockModeType;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentSession;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Lock;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Repository for Agent Gateway sessions.
|
||||
*/
|
||||
@Repository
|
||||
public interface AgentSessionDao extends JpaRepository<AgentSession, Long> {
|
||||
|
||||
/**
|
||||
* Find a session by deterministic session key.
|
||||
*/
|
||||
Optional<AgentSession> findBySessionKey(String sessionKey);
|
||||
|
||||
/**
|
||||
* Find a session by public session UID.
|
||||
*/
|
||||
Optional<AgentSession> findBySessionUid(String sessionUid);
|
||||
|
||||
Optional<AgentSession> findByIdAndWorkspaceIdAndChannelAndActorTypeAndActorIdAndOriginEntryType(
|
||||
Long id, String workspaceId, String channel, String actorType, String actorId, String originEntryType);
|
||||
|
||||
Optional<AgentSession> findBySessionUidAndWorkspaceIdAndChannelAndActorTypeAndActorIdAndOriginEntryType(
|
||||
String sessionUid, String workspaceId, String channel, String actorType, String actorId,
|
||||
String originEntryType);
|
||||
|
||||
Page<AgentSession> findByWorkspaceIdAndChannelAndActorTypeAndActorIdAndOriginEntryTypeOrderByGmtUpdateDesc(
|
||||
String workspaceId, String channel, String actorType, String actorId, String originEntryType,
|
||||
Pageable pageable);
|
||||
|
||||
Page<AgentSession>
|
||||
findByWorkspaceIdAndChannelAndActorTypeAndActorIdAndOriginEntryTypeAndTitleContainingIgnoreCaseOrderByGmtUpdateDesc(
|
||||
String workspaceId, String channel, String actorType, String actorId, String originEntryType,
|
||||
String title, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Lock a session row while assigning transcript append sequence.
|
||||
*/
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
Optional<AgentSession> findFirstById(Long id);
|
||||
|
||||
/**
|
||||
* Advance session activity without merging a stale session entity over transcript sequence or title updates.
|
||||
*/
|
||||
@Modifying(flushAutomatically = true)
|
||||
@Query("""
|
||||
update AgentSession session
|
||||
set session.gmtUpdate = case
|
||||
when session.gmtUpdate is null or session.gmtUpdate < :transitionAt then :transitionAt
|
||||
else session.gmtUpdate
|
||||
end
|
||||
where session.id = :sessionId
|
||||
""")
|
||||
int advanceGmtUpdate(@Param("sessionId") Long sessionId,
|
||||
@Param("transitionAt") java.time.LocalDateTime transitionAt);
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.conversation.persistence;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.common.entity.agent.AgentTranscriptEntry;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Repository for Agent Gateway transcript entries.
|
||||
*/
|
||||
@Repository
|
||||
public interface AgentTranscriptEntryDao extends JpaRepository<AgentTranscriptEntry, Long> {
|
||||
|
||||
/**
|
||||
* Find transcript entries for a session in append order.
|
||||
*/
|
||||
Page<AgentTranscriptEntry> findBySessionIdOrderBySessionSequenceAsc(Long sessionId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Find conversation messages for a session from newest to oldest.
|
||||
*/
|
||||
Page<AgentTranscriptEntry> findBySessionIdAndMessageRoleInOrderBySessionSequenceDesc(
|
||||
Long sessionId, List<String> messageRoles, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Find recent transcript entries for a session in reverse append order.
|
||||
*/
|
||||
List<AgentTranscriptEntry> findBySessionIdOrderBySessionSequenceDesc(Long sessionId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Find the latest transcript entry for a session and role.
|
||||
*/
|
||||
Optional<AgentTranscriptEntry> findTopBySessionIdAndMessageRoleOrderBySessionSequenceDesc(
|
||||
Long sessionId, String messageRole);
|
||||
|
||||
Optional<AgentTranscriptEntry> findFirstByRunIdAndMessageRoleOrderBySessionSequenceAsc(
|
||||
Long runId, String messageRole);
|
||||
|
||||
List<AgentTranscriptEntry> findTop2ByRunIdAndMessageRoleOrderBySessionSequenceAsc(
|
||||
Long runId, String messageRole);
|
||||
|
||||
Optional<AgentTranscriptEntry> findTopByRunIdAndMessageRoleOrderBySessionSequenceDesc(
|
||||
Long runId, String messageRole);
|
||||
|
||||
List<AgentTranscriptEntry> findByRunIdAndMessageRoleOrderBySessionSequenceAsc(
|
||||
Long runId, String messageRole);
|
||||
|
||||
/**
|
||||
* Find transcript entries from a session sequence in append order.
|
||||
*/
|
||||
List<AgentTranscriptEntry> findBySessionIdAndSessionSequenceGreaterThanEqualOrderBySessionSequenceAsc(
|
||||
Long sessionId, Long sessionSequence, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Delete transcript entries older than the configured retention boundary.
|
||||
*/
|
||||
long deleteByGmtCreateBefore(LocalDateTime cutoff);
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.identity;
|
||||
|
||||
import com.usthe.sureness.subject.PrincipalMap;
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
|
||||
/**
|
||||
* Shared actor normalization and projection helpers for Agent Gateway.
|
||||
*/
|
||||
public final class ActorSupport {
|
||||
|
||||
public static final String TYPE_USER = "user";
|
||||
|
||||
public static final String TYPE_SYSTEM = "system";
|
||||
|
||||
public static final String ID_ALERT_ANALYSIS = "alert-analysis";
|
||||
|
||||
public static final String ID_SCHEDULE = "schedule";
|
||||
|
||||
public static final String ROLE_ADMIN = "admin";
|
||||
|
||||
public static final String ROLE_USER = "user";
|
||||
|
||||
public static final String ROLE_GUEST = "guest";
|
||||
|
||||
public static final String ROLE_ALERT_ANALYSIS = "alert-analysis";
|
||||
|
||||
public static final String ROLE_SCHEDULE = "schedule";
|
||||
|
||||
private static final String ROLES_CLAIM = "roles";
|
||||
|
||||
private static final List<String> WELL_KNOWN_ROLES = List.of(ROLE_ADMIN, ROLE_USER, ROLE_GUEST);
|
||||
|
||||
private ActorSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a trusted user actor from the current Sureness context.
|
||||
*/
|
||||
public static AgentActor requireCurrentSurenessActor() {
|
||||
return requireSurenessActor(SurenessContextHolder.getBindSubject());
|
||||
}
|
||||
|
||||
/** Build an authenticated administrator actor from the current Sureness context. */
|
||||
public static AgentActor requireCurrentAdminSurenessActor() {
|
||||
AgentActor actor = requireCurrentSurenessActor();
|
||||
if (!actor.getRoles().contains(ROLE_ADMIN)) {
|
||||
throw new IllegalStateException("Administrator role is required");
|
||||
}
|
||||
return actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a trusted user actor from a Sureness subject and validate principal and roles.
|
||||
*/
|
||||
public static AgentActor requireSurenessActor(SubjectSum subject) {
|
||||
if (subject == null) {
|
||||
throw new IllegalStateException("Authenticated subject is required");
|
||||
}
|
||||
String principal = normalizeSubjectPrincipal(subject);
|
||||
List<String> roles = extractSurenessRoles(subject);
|
||||
if (roles.isEmpty()) {
|
||||
throw new IllegalStateException("Authenticated subject roles are required");
|
||||
}
|
||||
return AgentActor.builder()
|
||||
.type(TYPE_USER)
|
||||
.id(principal)
|
||||
.roles(roles)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Sureness roles from subject roles, JWT role claims, and hasRole checks.
|
||||
*/
|
||||
public static List<String> extractSurenessRoles(SubjectSum subject) {
|
||||
if (subject == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> roles = new ArrayList<>();
|
||||
addRoles(roles, subject.getRoles());
|
||||
PrincipalMap principalMap = subject.getPrincipalMap();
|
||||
if (principalMap != null) {
|
||||
addRoles(roles, principalMap.getPrincipal(ROLES_CLAIM));
|
||||
}
|
||||
WELL_KNOWN_ROLES.forEach(role -> addRoleIfPresent(subject, roles, role));
|
||||
return List.copyOf(roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the actor carries both type and id.
|
||||
*/
|
||||
public static boolean hasIdentity(AgentActor actor) {
|
||||
return actor != null && StringUtils.isNoneBlank(actor.getType(), actor.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize actor roles as JSON for fields that already store JSON role arrays.
|
||||
*/
|
||||
public static String rolesJson(AgentActor actor) {
|
||||
return actor == null ? null : JsonUtil.toJson(actor.getRoles());
|
||||
}
|
||||
|
||||
private static String normalizeSubjectPrincipal(SubjectSum subject) {
|
||||
Object principal = subject.getPrincipal();
|
||||
// Security subjects may expose padded textual principals; canonicalize before using them as actor identity.
|
||||
String normalized = GatewayText.normalize(principal == null ? null : String.valueOf(principal));
|
||||
if (normalized == null) {
|
||||
throw new IllegalStateException("Authenticated subject principal is required");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static void addRoles(List<String> roles, Object rawRoles) {
|
||||
if (rawRoles instanceof Collection<?> values) {
|
||||
values.forEach(value -> addRole(roles, value));
|
||||
return;
|
||||
}
|
||||
if (rawRoles instanceof String value) {
|
||||
for (String role : value.split(",")) {
|
||||
addRole(roles, role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addRoleIfPresent(SubjectSum subject, List<String> roles, String role) {
|
||||
if (subject.hasRole(role)) {
|
||||
addRole(roles, role);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addRole(List<String> roles, Object rawRole) {
|
||||
// Security frameworks may expose padded or comma-split role values; canonicalize before role comparison.
|
||||
String role = GatewayText.normalize(rawRole == null ? null : String.valueOf(rawRole));
|
||||
if (role != null && !roles.contains(role)) {
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user