mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17bd9f7e73 | ||
|
|
c805d633b6 | ||
|
|
c12372d82d | ||
|
|
63cc017bba | ||
|
|
9531a2fcd3 | ||
|
|
717521369c | ||
|
|
c8f2da472e | ||
|
|
6152c07320 | ||
|
|
b832d7ace4 | ||
|
|
3ab68b5814 | ||
|
|
1d9edb244c | ||
|
|
1429115282 | ||
|
|
4b217e7097 | ||
|
|
032a4852f7 | ||
|
|
6b78b784a2 | ||
|
|
11cd5bb0c7 | ||
|
|
a9eb231989 | ||
|
|
9b31baec8a | ||
|
|
0a38a73756 | ||
|
|
c9e3338c9f | ||
|
|
107f7d02ff | ||
|
|
e3acbbd6db | ||
|
|
31bc7d8a51 | ||
|
|
79427099a2 | ||
|
|
0edc363bcc | ||
|
|
47538dbc1a |
@@ -2713,6 +2713,210 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "orangeCatDeveloper",
|
||||
"name": "NekoPunch",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95899648?v=4",
|
||||
"profile": "https://github.com/orangeCatDeveloper",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wy471x",
|
||||
"name": "wy471x",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/52033069?v=4",
|
||||
"profile": "https://wy471x.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hengyuss",
|
||||
"name": "hengyuss",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/81064732?v=4",
|
||||
"profile": "https://github.com/hengyuss",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "moduvoice",
|
||||
"name": "moduvoice",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/291867022?v=4",
|
||||
"profile": "https://moduvoice.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hutiefang76",
|
||||
"name": "hutiefang76",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/137664623?v=4",
|
||||
"profile": "https://github.com/hutiefang76",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ZhouYinLong-lab",
|
||||
"name": "柳含知 Liu Hanzhi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/250777154?v=4",
|
||||
"profile": "https://zylatent.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wilmerdooley",
|
||||
"name": "wilmerdooley",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/259930736?v=4",
|
||||
"profile": "https://github.com/wilmerdooley",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Zmjjeff7",
|
||||
"name": "Zhenhong Guo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/175370943?v=4",
|
||||
"profile": "https://github.com/Zmjjeff7",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "abhyudayareddy",
|
||||
"name": "abhyudayareddy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/54602866?v=4",
|
||||
"profile": "https://github.com/abhyudayareddy",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "neon-hippo",
|
||||
"name": "neon-hippo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/165560498?v=4",
|
||||
"profile": "https://github.com/neon-hippo",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "P-Peaceful",
|
||||
"name": "P_Peaceful",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/52856161?v=4",
|
||||
"profile": "https://github.com/P-Peaceful",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zhusaidong",
|
||||
"name": "zhusaidong",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3039961?v=4",
|
||||
"profile": "https://github.com/zhusaidong",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zhehenlu",
|
||||
"name": "zhlu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31504542?v=4",
|
||||
"profile": "https://github.com/zhehenlu",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "brettgervasoni",
|
||||
"name": "brettgervasoni",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/34056000?v=4",
|
||||
"profile": "https://github.com/brettgervasoni",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Darshan-paul",
|
||||
"name": "Darshan-paul",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/211450705?v=4",
|
||||
"profile": "https://github.com/Darshan-paul",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "04cb",
|
||||
"name": "layla",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/111667698?v=4",
|
||||
"profile": "https://github.com/04cb",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "miantalha45",
|
||||
"name": "Talha Amjad",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/155809113?v=4",
|
||||
"profile": "https://github.com/miantalha45",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "turanalmammadov",
|
||||
"name": "Turan Almammadov",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16321061?v=4",
|
||||
"profile": "https://turanalmammadov.com/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zhaoyangplus",
|
||||
"name": "zhaoyangplus",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/245090302?v=4",
|
||||
"profile": "https://github.com/zhaoyangplus",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yexuanyang",
|
||||
"name": "Yang Yexuan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/73885401?v=4",
|
||||
"profile": "https://github.com/yexuanyang",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "markguo123",
|
||||
"name": "markguo123",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/155072651?v=4",
|
||||
"profile": "https://github.com/markguo123",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "leo-934",
|
||||
"name": "leo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/55838224?v=4",
|
||||
"profile": "https://github.com/leo-934",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"blog"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
cache-dependency-path: home/pnpm-lock.yaml
|
||||
- uses: actions/setup-python@v4
|
||||
|
||||
@@ -47,20 +47,20 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup pnpm (must run before setup-node so the pnpm cache can be configured)
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
# Setup Node.js environment
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
cache-dependency-path: home/pnpm-lock.yaml
|
||||
|
||||
# Setup pnpm
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
# Install dependencies in home directory
|
||||
- name: Install Dependencies
|
||||
working-directory: home
|
||||
|
||||
@@ -23,6 +23,7 @@ on:
|
||||
- cron: '0 0 * * *'
|
||||
push:
|
||||
branches: [ action* ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -56,3 +56,7 @@ application-mysql.yml
|
||||
application-pg.yml
|
||||
/package.json
|
||||
/yarn.lock
|
||||
|
||||
# Local Codex workspace files
|
||||
.codex/
|
||||
AGENTS.md
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Apache HertzBeat™
|
||||
Copyright 2024-2025 The Apache Software Foundation
|
||||
Copyright 2024-2026 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
@@ -49,52 +49,6 @@
|
||||
|
||||

|
||||
|
||||
## ⛄ Supported
|
||||
|
||||
> 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),
|
||||
[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),
|
||||
[Jvm](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jvm.yml), [SiteMap](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-fullsite.yml),
|
||||
[Ssl Certificate](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml), [SpringBoot2](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot2.yml),
|
||||
[FTP Server](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ftp.yml), [SpringBoot3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot3.yml),
|
||||
[Udp Port](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-udp_port.yml), [Dns](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dns.yml),
|
||||
[Pop3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-pop3.yml), [Ntp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ntp.yml),
|
||||
[Api Code](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api_code.yml), [Smtp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-smtp.yml),
|
||||
[Nginx](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nginx.yml)
|
||||
- [Mysql](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mysql.yml), [PostgreSQL](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-postgresql.yml),
|
||||
[MariaDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mariadb.yml), [Redis](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis.yml),
|
||||
[ElasticSearch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml), [SqlServer](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml),
|
||||
[Oracle](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-oracle.yml), [MongoDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mongodb.yml),
|
||||
[DM](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dm.yml), [OpenGauss](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opengauss.yml),
|
||||
[ClickHouse](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml), [IoTDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-iotdb.yml),
|
||||
[Redis Cluster](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml), [Redis Sentinel](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml)
|
||||
[Doris BE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_be.yml), [Doris FE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml),
|
||||
[Memcached](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-memcached.yml), [NebulaGraph](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml)
|
||||
- [Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-linux.yml), [Ubuntu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml),
|
||||
[CentOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-centos.yml), [Windows](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-windows.yml),
|
||||
[EulerOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-euleros.yml), [Fedora CoreOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-coreos.yml),
|
||||
[OpenSUSE](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opensuse.yml), [Rocky Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml),
|
||||
[Red Hat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redhat.yml), [FreeBSD](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-freebsd.yml),
|
||||
[AlmaLinux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-almalinux.yml), [Debian Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-debian.yml)
|
||||
- [Tomcat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tomcat.yml), [Nacos](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nacos.yml),
|
||||
[Zookeeper](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml), [RabbitMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml),
|
||||
[Flink](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-flink.yml), [Kafka](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kafka.yml),
|
||||
[ShenYu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-shenyu.yml), [DynamicTp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml),
|
||||
[Jetty](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jetty.yml), [ActiveMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-activemq.yml),
|
||||
[Spring Gateway](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml), [EMQX MQTT](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-emqx.yml),
|
||||
[AirFlow](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-airflow.yml), [Hive](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hive.yml),
|
||||
[Spark](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spark.yml), [Hadoop](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hadoop.yml)
|
||||
- [Kubernetes](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml), [Docker](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-docker.yml)
|
||||
- [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)
|
||||
- And More Your Custom Template.
|
||||
- Notified Support `Discord` `Slack` `Telegram` `Email` `DingTalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`.
|
||||
|
||||
|
||||
## 🐕 Quick Start
|
||||
|
||||
- If you wish to deploy HertzBeat locally, please refer to the following Deployment Documentation for instructions.
|
||||
@@ -551,6 +505,34 @@ Thanks to these wonderful people, welcome to join us:
|
||||
<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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeCatDeveloper"><img src="https://avatars.githubusercontent.com/u/95899648?v=4?s=100" width="100px;" alt="NekoPunch"/><br /><sub><b>NekoPunch</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wy471x.github.io/"><img src="https://avatars.githubusercontent.com/u/52033069?v=4?s=100" width="100px;" alt="wy471x"/><br /><sub><b>wy471x</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wy471x" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hengyuss"><img src="https://avatars.githubusercontent.com/u/81064732?v=4?s=100" width="100px;" alt="hengyuss"/><br /><sub><b>hengyuss</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hengyuss" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://moduvoice.com/"><img src="https://avatars.githubusercontent.com/u/291867022?v=4?s=100" width="100px;" alt="moduvoice"/><br /><sub><b>moduvoice</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moduvoice" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hutiefang76"><img src="https://avatars.githubusercontent.com/u/137664623?v=4?s=100" width="100px;" alt="hutiefang76"/><br /><sub><b>hutiefang76</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hutiefang76" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zylatent.com/"><img src="https://avatars.githubusercontent.com/u/250777154?v=4?s=100" width="100px;" alt="柳含知 Liu Hanzhi"/><br /><sub><b>柳含知 Liu Hanzhi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhouYinLong-lab" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilmerdooley"><img src="https://avatars.githubusercontent.com/u/259930736?v=4?s=100" width="100px;" alt="wilmerdooley"/><br /><sub><b>wilmerdooley</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wilmerdooley" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zmjjeff7"><img src="https://avatars.githubusercontent.com/u/175370943?v=4?s=100" width="100px;" alt="Zhenhong Guo"/><br /><sub><b>Zhenhong Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Zmjjeff7" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abhyudayareddy"><img src="https://avatars.githubusercontent.com/u/54602866?v=4?s=100" width="100px;" alt="abhyudayareddy"/><br /><sub><b>abhyudayareddy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=abhyudayareddy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neon-hippo"><img src="https://avatars.githubusercontent.com/u/165560498?v=4?s=100" width="100px;" alt="neon-hippo"/><br /><sub><b>neon-hippo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=neon-hippo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/P-Peaceful"><img src="https://avatars.githubusercontent.com/u/52856161?v=4?s=100" width="100px;" alt="P_Peaceful"/><br /><sub><b>P_Peaceful</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhusaidong"><img src="https://avatars.githubusercontent.com/u/3039961?v=4?s=100" width="100px;" alt="zhusaidong"/><br /><sub><b>zhusaidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhusaidong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhehenlu"><img src="https://avatars.githubusercontent.com/u/31504542?v=4?s=100" width="100px;" alt="zhlu"/><br /><sub><b>zhlu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brettgervasoni"><img src="https://avatars.githubusercontent.com/u/34056000?v=4?s=100" width="100px;" alt="brettgervasoni"/><br /><sub><b>brettgervasoni</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brettgervasoni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Darshan-paul"><img src="https://avatars.githubusercontent.com/u/211450705?v=4?s=100" width="100px;" alt="Darshan-paul"/><br /><sub><b>Darshan-paul</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Darshan-paul" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4?s=100" width="100px;" alt="layla"/><br /><sub><b>layla</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=04cb" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miantalha45"><img src="https://avatars.githubusercontent.com/u/155809113?v=4?s=100" width="100px;" alt="Talha Amjad"/><br /><sub><b>Talha Amjad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miantalha45" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://turanalmammadov.com/"><img src="https://avatars.githubusercontent.com/u/16321061?v=4?s=100" width="100px;" alt="Turan Almammadov"/><br /><sub><b>Turan Almammadov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhaoyangplus"><img src="https://avatars.githubusercontent.com/u/245090302?v=4?s=100" width="100px;" alt="zhaoyangplus"/><br /><sub><b>zhaoyangplus</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhaoyangplus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yexuanyang"><img src="https://avatars.githubusercontent.com/u/73885401?v=4?s=100" width="100px;" alt="Yang Yexuan"/><br /><sub><b>Yang Yexuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yexuanyang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markguo123"><img src="https://avatars.githubusercontent.com/u/155072651?v=4?s=100" width="100px;" alt="markguo123"/><br /><sub><b>markguo123</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=markguo123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leo-934"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="leo"/><br /><sub><b>leo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Documentation">📖</a> <a href="#blog-leo-934" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+42
-58
@@ -19,13 +19,13 @@
|
||||
[](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) 是 AI 驱动的下一代开源实时观测系统。指标日志统一收集,告警一站分发,智能管控分析。无需 Agent,高性能集群,提供强大的自定义监控和状态页构建能力。
|
||||
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是 AI 驱动的下一代开源实时观测系统。指标日志统一收集,告警一站分发,智能管控分析。无需 Agent,高性能集群,提供强大的自定义监控和状态页构建能力。
|
||||
|
||||
### 特点
|
||||
|
||||
@@ -39,70 +39,26 @@
|
||||
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
|
||||
|
||||
|
||||
> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求。
|
||||
> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求。
|
||||
|
||||
----
|
||||
|
||||
----
|
||||
|
||||
## 🥐 模块
|
||||
## 🥐 模块
|
||||
|
||||

|
||||

|
||||
|
||||
## ⛄ 已支持
|
||||
## 🐕 快速开始
|
||||
|
||||
> 我们将监控采集类型(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),
|
||||
[Jvm](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jvm.yml), [SiteMap](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-fullsite.yml),
|
||||
[Ssl Certificate](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml), [SpringBoot2](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot2.yml),
|
||||
[FTP Server](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ftp.yml), [SpringBoot3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot3.yml),
|
||||
[Udp Port](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-udp_port.yml), [Dns](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dns.yml),
|
||||
[Pop3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-pop3.yml), [Ntp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ntp.yml),
|
||||
[Api Code](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api_code.yml), [Smtp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-smtp.yml),
|
||||
[Nginx](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nginx.yml)
|
||||
- [Mysql](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mysql.yml), [PostgreSQL](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-postgresql.yml),
|
||||
[MariaDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mariadb.yml), [Redis](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis.yml),
|
||||
[ElasticSearch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml), [SqlServer](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml),
|
||||
[Oracle](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-oracle.yml), [MongoDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mongodb.yml),
|
||||
[DM](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dm.yml), [OpenGauss](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opengauss.yml),
|
||||
[ClickHouse](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml), [IoTDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-iotdb.yml),
|
||||
[Redis Cluster](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml), [Redis Sentinel](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml)
|
||||
[Doris BE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_be.yml), [Doris FE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml),
|
||||
[Memcached](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-memcached.yml), [NebulaGraph](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml)
|
||||
- [Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-linux.yml), [Ubuntu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml),
|
||||
[CentOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-centos.yml), [Windows](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-windows.yml),
|
||||
[EulerOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-euleros.yml), [Fedora CoreOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-coreos.yml),
|
||||
[OpenSUSE](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opensuse.yml), [Rocky Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml),
|
||||
[Red Hat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redhat.yml), [FreeBSD](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-freebsd.yml),
|
||||
[AlmaLinux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-almalinux.yml), [Debian Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-debian.yml)
|
||||
- [Tomcat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tomcat.yml), [Nacos](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nacos.yml),
|
||||
[Zookeeper](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml), [RabbitMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml),
|
||||
[Flink](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-flink.yml), [Kafka](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kafka.yml),
|
||||
[ShenYu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-shenyu.yml), [DynamicTp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml),
|
||||
[Jetty](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jetty.yml), [ActiveMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-activemq.yml),
|
||||
[Spring Gateway](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml), [EMQX MQTT](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-emqx.yml),
|
||||
[AirFlow](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-airflow.yml), [Hive](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hive.yml),
|
||||
[Spark](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spark.yml), [Hadoop](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hadoop.yml)
|
||||
- [Kubernetes](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml), [Docker](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-docker.yml)
|
||||
- [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酱`。
|
||||
|
||||
## 🐕 快速开始
|
||||
|
||||
- 如果您是想将 HertzBeat 部署到内网环境搭建监控系统,请参考下面的部署文档进行操作。
|
||||
- 如果您是想将 HertzBeat 部署到内网环境搭建监控系统,请参考下面的部署文档进行操作。
|
||||
|
||||
### 🍞 HertzBeat 安装
|
||||
> HertzBeat 支持通过源码安装启动,Docker 容器运行和安装包方式安装部署,CPU 架构支持 x86/arm64。
|
||||
|
||||
##### 方式一:Docker 方式快速安装
|
||||
##### 方式一:Docker 方式快速安装
|
||||
|
||||
1. `docker` 环境仅需一条命令即可开始
|
||||
1. `docker` 环境仅需一条命令即可开始
|
||||
|
||||
```shell
|
||||
docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat
|
||||
@@ -164,7 +120,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)
|
||||
|
||||
##### 方式五:Kubernetes Helm Charts 部署 hertzbeat+collector+postgresql+tsdb
|
||||
|
||||
@@ -176,8 +132,8 @@
|
||||
|
||||
## ✨ 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 -->
|
||||
@@ -548,6 +504,34 @@ Thanks these wonderful people, welcome to join us:
|
||||
<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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeCatDeveloper"><img src="https://avatars.githubusercontent.com/u/95899648?v=4?s=100" width="100px;" alt="NekoPunch"/><br /><sub><b>NekoPunch</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wy471x.github.io/"><img src="https://avatars.githubusercontent.com/u/52033069?v=4?s=100" width="100px;" alt="wy471x"/><br /><sub><b>wy471x</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wy471x" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hengyuss"><img src="https://avatars.githubusercontent.com/u/81064732?v=4?s=100" width="100px;" alt="hengyuss"/><br /><sub><b>hengyuss</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hengyuss" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://moduvoice.com/"><img src="https://avatars.githubusercontent.com/u/291867022?v=4?s=100" width="100px;" alt="moduvoice"/><br /><sub><b>moduvoice</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moduvoice" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hutiefang76"><img src="https://avatars.githubusercontent.com/u/137664623?v=4?s=100" width="100px;" alt="hutiefang76"/><br /><sub><b>hutiefang76</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hutiefang76" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zylatent.com/"><img src="https://avatars.githubusercontent.com/u/250777154?v=4?s=100" width="100px;" alt="柳含知 Liu Hanzhi"/><br /><sub><b>柳含知 Liu Hanzhi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhouYinLong-lab" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilmerdooley"><img src="https://avatars.githubusercontent.com/u/259930736?v=4?s=100" width="100px;" alt="wilmerdooley"/><br /><sub><b>wilmerdooley</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wilmerdooley" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zmjjeff7"><img src="https://avatars.githubusercontent.com/u/175370943?v=4?s=100" width="100px;" alt="Zhenhong Guo"/><br /><sub><b>Zhenhong Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Zmjjeff7" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abhyudayareddy"><img src="https://avatars.githubusercontent.com/u/54602866?v=4?s=100" width="100px;" alt="abhyudayareddy"/><br /><sub><b>abhyudayareddy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=abhyudayareddy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neon-hippo"><img src="https://avatars.githubusercontent.com/u/165560498?v=4?s=100" width="100px;" alt="neon-hippo"/><br /><sub><b>neon-hippo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=neon-hippo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/P-Peaceful"><img src="https://avatars.githubusercontent.com/u/52856161?v=4?s=100" width="100px;" alt="P_Peaceful"/><br /><sub><b>P_Peaceful</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhusaidong"><img src="https://avatars.githubusercontent.com/u/3039961?v=4?s=100" width="100px;" alt="zhusaidong"/><br /><sub><b>zhusaidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhusaidong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhehenlu"><img src="https://avatars.githubusercontent.com/u/31504542?v=4?s=100" width="100px;" alt="zhlu"/><br /><sub><b>zhlu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brettgervasoni"><img src="https://avatars.githubusercontent.com/u/34056000?v=4?s=100" width="100px;" alt="brettgervasoni"/><br /><sub><b>brettgervasoni</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brettgervasoni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Darshan-paul"><img src="https://avatars.githubusercontent.com/u/211450705?v=4?s=100" width="100px;" alt="Darshan-paul"/><br /><sub><b>Darshan-paul</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Darshan-paul" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4?s=100" width="100px;" alt="layla"/><br /><sub><b>layla</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=04cb" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miantalha45"><img src="https://avatars.githubusercontent.com/u/155809113?v=4?s=100" width="100px;" alt="Talha Amjad"/><br /><sub><b>Talha Amjad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miantalha45" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://turanalmammadov.com/"><img src="https://avatars.githubusercontent.com/u/16321061?v=4?s=100" width="100px;" alt="Turan Almammadov"/><br /><sub><b>Turan Almammadov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhaoyangplus"><img src="https://avatars.githubusercontent.com/u/245090302?v=4?s=100" width="100px;" alt="zhaoyangplus"/><br /><sub><b>zhaoyangplus</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhaoyangplus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yexuanyang"><img src="https://avatars.githubusercontent.com/u/73885401?v=4?s=100" width="100px;" alt="Yang Yexuan"/><br /><sub><b>Yang Yexuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yexuanyang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markguo123"><img src="https://avatars.githubusercontent.com/u/155072651?v=4?s=100" width="100px;" alt="markguo123"/><br /><sub><b>markguo123</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=markguo123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leo-934"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="leo"/><br /><sub><b>leo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Documentation">📖</a> <a href="#blog-leo-934" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -555,11 +539,11 @@ Thanks these wonderful people, welcome to join us:
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
## 💬 社区交流
|
||||
|
||||
##### Channel
|
||||
##### Channel
|
||||
|
||||
[订阅邮件列表](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : 发送邮件至 `dev-subscribe@hertzbeat.apache.org` 来订阅邮件列表.
|
||||
|
||||
|
||||
+28
-44
@@ -48,50 +48,6 @@
|
||||
|
||||

|
||||
|
||||
## ⛄ 利用可能
|
||||
|
||||
> `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),
|
||||
[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),
|
||||
[Jvm](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jvm.yml), [SiteMap](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-fullsite.yml),
|
||||
[Ssl Certificate](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml), [SpringBoot2](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot2.yml),
|
||||
[FTP Server](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ftp.yml), [SpringBoot3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot3.yml),
|
||||
[Udp Port](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-udp_port.yml), [Dns](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dns.yml),
|
||||
[Pop3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-pop3.yml), [Ntp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ntp.yml),
|
||||
[Api Code](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api_code.yml), [Smtp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-smtp.yml),
|
||||
[Nginx](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nginx.yml)
|
||||
- [Mysql](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mysql.yml), [PostgreSQL](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-postgresql.yml),
|
||||
[MariaDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mariadb.yml), [Redis](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis.yml),
|
||||
[ElasticSearch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml), [SqlServer](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml),
|
||||
[Oracle](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-oracle.yml), [MongoDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mongodb.yml),
|
||||
[DM](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dm.yml), [OpenGauss](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opengauss.yml),
|
||||
[ClickHouse](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml), [IoTDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-iotdb.yml),
|
||||
[Redis Cluster](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml), [Redis Sentinel](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml)
|
||||
[Doris BE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_be.yml), [Doris FE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml),
|
||||
[Memcached](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-memcached.yml), [NebulaGraph](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml)
|
||||
- [Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-linux.yml), [Ubuntu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml),
|
||||
[CentOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-centos.yml), [Windows](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-windows.yml),
|
||||
[EulerOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-euleros.yml), [Fedora CoreOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-coreos.yml),
|
||||
[OpenSUSE](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opensuse.yml), [Rocky Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml),
|
||||
[Red Hat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redhat.yml), [FreeBSD](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-freebsd.yml),
|
||||
[AlmaLinux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-almalinux.yml), [Debian Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-debian.yml)
|
||||
- [Tomcat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tomcat.yml), [Nacos](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nacos.yml),
|
||||
[Zookeeper](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml), [RabbitMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml),
|
||||
[Flink](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-flink.yml), [Kafka](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kafka.yml),
|
||||
[ShenYu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-shenyu.yml), [DynamicTp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml),
|
||||
[Jetty](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jetty.yml), [ActiveMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-activemq.yml),
|
||||
[Spring Gateway](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml), [EMQX MQTT](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-emqx.yml),
|
||||
[AirFlow](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-airflow.yml), [Hive](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hive.yml),
|
||||
[Spark](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spark.yml), [Hadoop](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hadoop.yml)
|
||||
- [Kubernetes](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml), [Docker](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-docker.yml)
|
||||
- [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` `Email` `DingTalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`。
|
||||
|
||||
## 🐕 クイックスタート
|
||||
|
||||
- HertzBeat をローカルに展開する場合は、以下のデプロイメントドキュメントを参照してください。
|
||||
@@ -551,6 +507,34 @@ Thanks these wonderful people, welcome to join us:
|
||||
<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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeCatDeveloper"><img src="https://avatars.githubusercontent.com/u/95899648?v=4?s=100" width="100px;" alt="NekoPunch"/><br /><sub><b>NekoPunch</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wy471x.github.io/"><img src="https://avatars.githubusercontent.com/u/52033069?v=4?s=100" width="100px;" alt="wy471x"/><br /><sub><b>wy471x</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wy471x" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hengyuss"><img src="https://avatars.githubusercontent.com/u/81064732?v=4?s=100" width="100px;" alt="hengyuss"/><br /><sub><b>hengyuss</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hengyuss" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://moduvoice.com/"><img src="https://avatars.githubusercontent.com/u/291867022?v=4?s=100" width="100px;" alt="moduvoice"/><br /><sub><b>moduvoice</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moduvoice" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hutiefang76"><img src="https://avatars.githubusercontent.com/u/137664623?v=4?s=100" width="100px;" alt="hutiefang76"/><br /><sub><b>hutiefang76</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hutiefang76" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zylatent.com/"><img src="https://avatars.githubusercontent.com/u/250777154?v=4?s=100" width="100px;" alt="柳含知 Liu Hanzhi"/><br /><sub><b>柳含知 Liu Hanzhi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhouYinLong-lab" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilmerdooley"><img src="https://avatars.githubusercontent.com/u/259930736?v=4?s=100" width="100px;" alt="wilmerdooley"/><br /><sub><b>wilmerdooley</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wilmerdooley" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zmjjeff7"><img src="https://avatars.githubusercontent.com/u/175370943?v=4?s=100" width="100px;" alt="Zhenhong Guo"/><br /><sub><b>Zhenhong Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Zmjjeff7" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abhyudayareddy"><img src="https://avatars.githubusercontent.com/u/54602866?v=4?s=100" width="100px;" alt="abhyudayareddy"/><br /><sub><b>abhyudayareddy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=abhyudayareddy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neon-hippo"><img src="https://avatars.githubusercontent.com/u/165560498?v=4?s=100" width="100px;" alt="neon-hippo"/><br /><sub><b>neon-hippo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=neon-hippo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/P-Peaceful"><img src="https://avatars.githubusercontent.com/u/52856161?v=4?s=100" width="100px;" alt="P_Peaceful"/><br /><sub><b>P_Peaceful</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhusaidong"><img src="https://avatars.githubusercontent.com/u/3039961?v=4?s=100" width="100px;" alt="zhusaidong"/><br /><sub><b>zhusaidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhusaidong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhehenlu"><img src="https://avatars.githubusercontent.com/u/31504542?v=4?s=100" width="100px;" alt="zhlu"/><br /><sub><b>zhlu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brettgervasoni"><img src="https://avatars.githubusercontent.com/u/34056000?v=4?s=100" width="100px;" alt="brettgervasoni"/><br /><sub><b>brettgervasoni</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brettgervasoni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Darshan-paul"><img src="https://avatars.githubusercontent.com/u/211450705?v=4?s=100" width="100px;" alt="Darshan-paul"/><br /><sub><b>Darshan-paul</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Darshan-paul" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4?s=100" width="100px;" alt="layla"/><br /><sub><b>layla</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=04cb" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miantalha45"><img src="https://avatars.githubusercontent.com/u/155809113?v=4?s=100" width="100px;" alt="Talha Amjad"/><br /><sub><b>Talha Amjad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miantalha45" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://turanalmammadov.com/"><img src="https://avatars.githubusercontent.com/u/16321061?v=4?s=100" width="100px;" alt="Turan Almammadov"/><br /><sub><b>Turan Almammadov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhaoyangplus"><img src="https://avatars.githubusercontent.com/u/245090302?v=4?s=100" width="100px;" alt="zhaoyangplus"/><br /><sub><b>zhaoyangplus</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhaoyangplus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yexuanyang"><img src="https://avatars.githubusercontent.com/u/73885401?v=4?s=100" width="100px;" alt="Yang Yexuan"/><br /><sub><b>Yang Yexuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yexuanyang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markguo123"><img src="https://avatars.githubusercontent.com/u/155072651?v=4?s=100" width="100px;" alt="markguo123"/><br /><sub><b>markguo123</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=markguo123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leo-934"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="leo"/><br /><sub><b>leo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Documentation">📖</a> <a href="#blog-leo-934" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -18,14 +18,21 @@
|
||||
package org.apache.hertzbeat.ai.config;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import org.springframework.core.NamedInheritableThreadLocal;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.core.NamedThreadLocal;
|
||||
|
||||
/**
|
||||
* Context holder for AI agent security context.
|
||||
*/
|
||||
public final class McpContextHolder {
|
||||
|
||||
static final String SUBJECT_CONTEXT_KEY = McpContextHolder.class.getName() + ".subject";
|
||||
|
||||
private static final ThreadLocal<SubjectSum> subjectHolder =
|
||||
new NamedInheritableThreadLocal<>("MCP Security and User Identification Context");
|
||||
new NamedThreadLocal<>("MCP Security and User Identification Context");
|
||||
|
||||
private McpContextHolder() {}
|
||||
|
||||
@@ -44,10 +51,73 @@ public final class McpContextHolder {
|
||||
return subjectHolder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a security context that is propagated only through the Spring AI tool invocation chain.
|
||||
*
|
||||
* @param subject current authenticated subject, may be {@code null}
|
||||
* @return tool context without null values
|
||||
*/
|
||||
public static Map<String, Object> createToolContext(SubjectSum subject) {
|
||||
return subject == null ? Map.of() : Map.of(SUBJECT_CONTEXT_KEY, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and validates the authenticated subject from the Spring AI tool context.
|
||||
*
|
||||
* @param toolContext Spring AI tool context
|
||||
* @return authenticated subject, or {@code null} when absent
|
||||
*/
|
||||
public static SubjectSum getSubject(ToolContext toolContext) {
|
||||
if (toolContext == null) {
|
||||
return null;
|
||||
}
|
||||
Object subject = toolContext.getContext().get(SUBJECT_CONTEXT_KEY);
|
||||
return subject instanceof SubjectSum subjectSum ? subjectSum : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a synchronous tool invocation within the given subject scope and restores the original thread
|
||||
* contexts afterward.
|
||||
*
|
||||
* <p>Spring AI tool callbacks may run on pooled threads, so they cannot rely on a {@link ThreadLocal} left on
|
||||
* the Servlet request thread. This method binds both MCP and Sureness contexts so the tool and downstream
|
||||
* services observe the same user identity.</p>
|
||||
*
|
||||
* @param subject current request subject, may be {@code null}
|
||||
* @param operation synchronous tool invocation
|
||||
* @param <T> tool result type
|
||||
* @return tool invocation result
|
||||
*/
|
||||
public static <T> T callWithSubject(SubjectSum subject, Supplier<T> operation) {
|
||||
SubjectSum previousMcpSubject = getSubject();
|
||||
SubjectSum previousSurenessSubject = SurenessContextHolder.getBindSubject();
|
||||
replaceSubjects(subject);
|
||||
try {
|
||||
return operation.get();
|
||||
} finally {
|
||||
replaceSubjects(previousMcpSubject, previousSurenessSubject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the context from the thread to prevent memory leaks.
|
||||
*/
|
||||
public static void clear() {
|
||||
subjectHolder.remove();
|
||||
}
|
||||
|
||||
private static void replaceSubjects(SubjectSum subject) {
|
||||
replaceSubjects(subject, subject);
|
||||
}
|
||||
|
||||
private static void replaceSubjects(SubjectSum mcpSubject, SubjectSum surenessSubject) {
|
||||
clear();
|
||||
SurenessContextHolder.unbindSubject();
|
||||
if (mcpSubject != null) {
|
||||
setSubject(mcpSubject);
|
||||
}
|
||||
if (surenessSubject != null) {
|
||||
SurenessContextHolder.bindSubject(surenessSubject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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.config;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import org.springframework.ai.tool.metadata.ToolMetadata;
|
||||
|
||||
/**
|
||||
* Establishes a request-scoped security context for Spring AI tool callbacks.
|
||||
*
|
||||
* <p>The wrapper keeps the tool definition and metadata unchanged. It binds the identity only for the delegated
|
||||
* call and restores the worker thread's original state after both successful and failed invocations.</p>
|
||||
*/
|
||||
public final class SecurityContextToolCallback implements ToolCallback {
|
||||
|
||||
private final ToolCallback delegate;
|
||||
|
||||
public SecurityContextToolCallback(ToolCallback delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return delegate.getToolDefinition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolMetadata getToolMetadata() {
|
||||
return delegate.getToolMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput) {
|
||||
return call(toolInput, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput, ToolContext toolContext) {
|
||||
return McpContextHolder.callWithSubject(
|
||||
McpContextHolder.getSubject(toolContext),
|
||||
() -> delegate.call(toolInput, toolContext));
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,10 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.controller;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.SecurityData;
|
||||
@@ -75,8 +72,6 @@ public class ChatController {
|
||||
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(@Valid @RequestBody ChatRequestContext context) {
|
||||
try {
|
||||
// Validate message is not empty
|
||||
SubjectSum subject = SurenessContextHolder.getBindSubject();
|
||||
McpContextHolder.setSubject(subject);
|
||||
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
|
||||
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
|
||||
+10
-2
@@ -18,12 +18,13 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.pojo.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||
|
||||
/**
|
||||
@@ -47,4 +48,11 @@ public class ChatRequestContext {
|
||||
* Conversation history messages for context
|
||||
*/
|
||||
private List<ChatMessage> conversationHistory;
|
||||
|
||||
/**
|
||||
* Authenticated subject captured by the server. It cannot be supplied through client JSON and is never included
|
||||
* in model messages.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private SubjectSum subject;
|
||||
}
|
||||
|
||||
+38
-13
@@ -19,10 +19,13 @@
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.config.SecurityContextToolCallback;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
|
||||
@@ -45,9 +48,11 @@ import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.util.StringUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -69,7 +74,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
|
||||
private final GeneralConfigDao generalConfigDao;
|
||||
|
||||
private ModelProviderConfig modelProviderConfig;
|
||||
private volatile ModelProviderConfig modelProviderConfig;
|
||||
|
||||
|
||||
private final SkillRegistry skillRegistry;
|
||||
@@ -78,7 +83,9 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
@Qualifier("hertzbeatTools")
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
private boolean isConfigured = false;
|
||||
private volatile boolean configured;
|
||||
|
||||
private volatile boolean configurationLoaded;
|
||||
|
||||
@Value("classpath:/prompt/system-message.st")
|
||||
private Resource systemResource;
|
||||
@@ -122,11 +129,15 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
// Build system prompt with dynamic skills list and conversation ID
|
||||
// The conversationId is injected into the prompt so AI can pass it to schedule tools
|
||||
String systemPrompt = buildSystemPrompt(context.getConversationId());
|
||||
ToolCallback[] toolCallbacks = Arrays.stream(toolCallbackProvider.getToolCallbacks())
|
||||
.map(SecurityContextToolCallback::new)
|
||||
.toArray(ToolCallback[]::new);
|
||||
|
||||
return chatClient.prompt()
|
||||
.messages(messages)
|
||||
.system(systemPrompt)
|
||||
.tools(toolCallbackProvider)
|
||||
.tools((Object[]) toolCallbacks)
|
||||
.toolContext(McpContextHolder.createToolContext(context.getSubject()))
|
||||
.stream()
|
||||
.content()
|
||||
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
|
||||
@@ -150,7 +161,8 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
.replace(CONVERSATION_ID_PLACEHOLDER, String.valueOf(conversationId));
|
||||
|
||||
// add extra prompt for protected model to guide it to use protected tools
|
||||
if (Objects.equals(modelProviderConfig.getParticipationModel(), "PROTECTED")) {
|
||||
ModelProviderConfig currentConfig = modelProviderConfig;
|
||||
if (currentConfig != null && Objects.equals(currentConfig.getParticipationModel(), "PROTECTED")) {
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("conversationId", conversationId);
|
||||
return template + SystemPromptTemplate.builder().resource(extraResourceProtected).build()
|
||||
@@ -202,19 +214,32 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
|
||||
@EventListener(AiProviderConfigChangeEvent.class)
|
||||
public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
|
||||
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
|
||||
this.modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
|
||||
refreshProviderConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigured() {
|
||||
if (!isConfigured) {
|
||||
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
|
||||
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(),
|
||||
ModelProviderConfig.class);
|
||||
isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null;
|
||||
this.modelProviderConfig = modelProviderConfig;
|
||||
if (!configurationLoaded) {
|
||||
synchronized (this) {
|
||||
if (!configurationLoaded) {
|
||||
refreshProviderConfiguration();
|
||||
}
|
||||
}
|
||||
}
|
||||
return isConfigured;
|
||||
return configured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically refreshes the configuration snapshot after enabling, disabling, or switching providers.
|
||||
*/
|
||||
private synchronized void refreshProviderConfiguration() {
|
||||
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
|
||||
ModelProviderConfig refreshedConfig = null;
|
||||
if (providerConfig != null && StringUtils.hasText(providerConfig.getContent())) {
|
||||
refreshedConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
|
||||
}
|
||||
modelProviderConfig = refreshedConfig;
|
||||
configured = refreshedConfig != null && StringUtils.hasText(refreshedConfig.getApiKey());
|
||||
configurationLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -17,6 +17,8 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
|
||||
@@ -104,6 +106,8 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
// Stream response from AI service
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
ChatMessage finalChatMessage = chatMessage;
|
||||
SubjectSum subject = SurenessContextHolder.getBindSubject();
|
||||
context.setSubject(subject);
|
||||
return chatClientProviderService.streamChat(context)
|
||||
.map(chunk -> {
|
||||
fullResponse.append(chunk);
|
||||
|
||||
@@ -41,9 +41,6 @@ import reactor.core.publisher.Flux;
|
||||
@Service
|
||||
public class SopEngineImpl implements SopEngine {
|
||||
|
||||
// Thread-local context for each execution
|
||||
private static final ThreadLocal<Map<String, Object>> CONTEXT_BUS = ThreadLocal.withInitial(HashMap::new);
|
||||
|
||||
private final List<SopExecutor> executors;
|
||||
|
||||
@Autowired
|
||||
@@ -55,22 +52,12 @@ public class SopEngineImpl implements SopEngine {
|
||||
public Flux<String> execute(SopDefinition definition, Map<String, Object> inputParams) {
|
||||
return Flux.create(sink -> {
|
||||
try {
|
||||
OutputConfig outputConfig = resolveOutputConfig(definition);
|
||||
Map<String, Object> context = prepareContext(definition, inputParams, outputConfig);
|
||||
|
||||
log.info("Starting execution of SOP: {}", definition.getName());
|
||||
sink.next("Starting SOP: " + definition.getName() + " (v" + definition.getVersion() + ")");
|
||||
|
||||
// Initialize context with input parameters
|
||||
Map<String, Object> context = CONTEXT_BUS.get();
|
||||
context.clear();
|
||||
context.putAll(inputParams);
|
||||
|
||||
// Add language configuration to context
|
||||
OutputConfig outputConfig = definition.getOutput();
|
||||
if (outputConfig != null) {
|
||||
context.put("_language", outputConfig.getLanguageCode());
|
||||
} else {
|
||||
context.put("_language", "zh");
|
||||
}
|
||||
|
||||
for (SopStep step : definition.getSteps()) {
|
||||
sink.next("Executing step [" + step.getId() + "]: " + step.getType());
|
||||
|
||||
@@ -106,10 +93,9 @@ public class SopEngineImpl implements SopEngine {
|
||||
sink.next("SOP " + definition.getName() + " completed successfully.");
|
||||
sink.complete();
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing SOP {}: {}", definition.getName(), e.getMessage(), e);
|
||||
String sopName = definition == null ? "unknown" : definition.getName();
|
||||
log.error("Error executing SOP {}: {}", sopName, e.getMessage(), e);
|
||||
sink.error(e);
|
||||
} finally {
|
||||
CONTEXT_BUS.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -118,40 +104,20 @@ public class SopEngineImpl implements SopEngine {
|
||||
public SopResult executeSync(SopDefinition definition, Map<String, Object> inputParams) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
List<StepResult> stepResults = new ArrayList<>();
|
||||
Map<String, Object> context = new HashMap<>(inputParams);
|
||||
|
||||
// Apply default values for parameters that are not provided
|
||||
if (definition.getParameters() != null) {
|
||||
for (SopParameter param : definition.getParameters()) {
|
||||
if (!context.containsKey(param.getName()) && param.getDefaultValue() != null) {
|
||||
context.put(param.getName(), param.getDefaultValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get output configuration first
|
||||
OutputConfig outputConfig = definition.getOutput();
|
||||
if (outputConfig == null) {
|
||||
outputConfig = OutputConfig.builder()
|
||||
.type("simple")
|
||||
.format("text")
|
||||
.language("zh")
|
||||
.build();
|
||||
}
|
||||
|
||||
// Add language configuration to context
|
||||
context.put("_language", outputConfig.getLanguageCode());
|
||||
|
||||
|
||||
SopResult.SopResultBuilder resultBuilder = SopResult.builder()
|
||||
.sopName(definition.getName())
|
||||
.sopVersion(definition.getVersion())
|
||||
.sopName(definition == null ? null : definition.getName())
|
||||
.sopVersion(definition == null ? null : definition.getVersion())
|
||||
.startTime(startTime);
|
||||
|
||||
resultBuilder.outputType(outputConfig.getOutputType());
|
||||
resultBuilder.outputFormat(outputConfig.getFormat() != null ? outputConfig.getFormat() : "text");
|
||||
resultBuilder.language(outputConfig.getLanguageCode());
|
||||
|
||||
|
||||
try {
|
||||
OutputConfig outputConfig = resolveOutputConfig(definition);
|
||||
Map<String, Object> context = prepareContext(definition, inputParams, outputConfig);
|
||||
|
||||
resultBuilder.outputType(outputConfig.getOutputType());
|
||||
resultBuilder.outputFormat(outputConfig.getFormat() != null ? outputConfig.getFormat() : "text");
|
||||
resultBuilder.language(outputConfig.getLanguageCode());
|
||||
|
||||
log.info("Starting sync execution of SOP: {}", definition.getName());
|
||||
|
||||
for (SopStep step : definition.getSteps()) {
|
||||
@@ -213,10 +179,55 @@ public class SopEngineImpl implements SopEngine {
|
||||
return resultBuilder.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing SOP {}: {}", definition.getName(), e.getMessage(), e);
|
||||
String sopName = definition == null ? "unknown" : definition.getName();
|
||||
log.error("Error executing SOP {}: {}", sopName, e.getMessage(), e);
|
||||
return buildFailedResult(resultBuilder, stepResults, e.getMessage(), startTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies defaults and validates required parameters consistently for every execution entry point.
|
||||
*/
|
||||
private Map<String, Object> prepareContext(SopDefinition definition, Map<String, Object> inputParams,
|
||||
OutputConfig outputConfig) {
|
||||
if (definition.getSteps() == null || definition.getSteps().isEmpty()) {
|
||||
throw new IllegalArgumentException("SOP must contain at least one step");
|
||||
}
|
||||
|
||||
Map<String, Object> context = inputParams == null ? new HashMap<>() : new HashMap<>(inputParams);
|
||||
if (definition.getParameters() != null) {
|
||||
for (SopParameter parameter : definition.getParameters()) {
|
||||
Object value = context.get(parameter.getName());
|
||||
if (isMissing(value) && parameter.getDefaultValue() != null) {
|
||||
context.put(parameter.getName(), parameter.getDefaultValue());
|
||||
value = parameter.getDefaultValue();
|
||||
}
|
||||
if (parameter.isRequired() && isMissing(value)) {
|
||||
throw new IllegalArgumentException("Required SOP parameter is missing: " + parameter.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
context.put("_language", outputConfig.getLanguageCode());
|
||||
return context;
|
||||
}
|
||||
|
||||
private boolean isMissing(Object value) {
|
||||
return value == null || value instanceof String text && text.isBlank();
|
||||
}
|
||||
|
||||
private OutputConfig resolveOutputConfig(SopDefinition definition) {
|
||||
if (definition == null) {
|
||||
throw new IllegalArgumentException("SOP definition must not be null");
|
||||
}
|
||||
if (definition.getOutput() != null) {
|
||||
return definition.getOutput();
|
||||
}
|
||||
return OutputConfig.builder()
|
||||
.type("simple")
|
||||
.format("text")
|
||||
.language("zh")
|
||||
.build();
|
||||
}
|
||||
|
||||
private SopResult buildFailedResult(SopResult.SopResultBuilder builder,
|
||||
List<StepResult> stepResults, String error, long startTime) {
|
||||
|
||||
+61
-20
@@ -17,8 +17,16 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.registry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.ai.sop.engine.SopEngine;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
|
||||
@@ -49,33 +57,66 @@ public class SopToolCallback implements ToolCallback {
|
||||
|
||||
@Override
|
||||
public String call(String arguments) {
|
||||
// TODO: Parse arguments and execute SOP
|
||||
return "SOP " + definition.getName() + " execution started.";
|
||||
Map<String, Object> inputParams = parseArguments(arguments);
|
||||
SopResult result = sopEngine.executeSync(definition, inputParams);
|
||||
return result.toAiResponse();
|
||||
}
|
||||
|
||||
private String buildInputSchema() {
|
||||
// Build JSON Schema for SOP parameters
|
||||
StringBuilder schema = new StringBuilder();
|
||||
schema.append("{\"type\":\"object\",\"properties\":{");
|
||||
|
||||
if (definition.getParameters() != null && !definition.getParameters().isEmpty()) {
|
||||
boolean first = true;
|
||||
for (var param : definition.getParameters()) {
|
||||
if (!first) {
|
||||
schema.append(",");
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
List<String> required = new ArrayList<>();
|
||||
|
||||
if (definition.getParameters() != null) {
|
||||
for (SopParameter parameter : definition.getParameters()) {
|
||||
Map<String, Object> property = new LinkedHashMap<>();
|
||||
property.put("type", mapType(parameter.getType()));
|
||||
if (parameter.getDescription() != null) {
|
||||
property.put("description", parameter.getDescription());
|
||||
}
|
||||
schema.append("\"").append(param.getName()).append("\":");
|
||||
schema.append("{\"type\":\"").append(mapType(param.getType())).append("\"");
|
||||
if (param.getDescription() != null) {
|
||||
schema.append(",\"description\":\"").append(param.getDescription()).append("\"");
|
||||
if (parameter.getDefaultValue() != null) {
|
||||
property.put("default", convertDefaultValue(parameter));
|
||||
}
|
||||
properties.put(parameter.getName(), property);
|
||||
if (parameter.isRequired()) {
|
||||
required.add(parameter.getName());
|
||||
}
|
||||
schema.append("}");
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
schema.append("}}");
|
||||
return schema.toString();
|
||||
|
||||
schema.put("type", "object");
|
||||
schema.put("properties", properties);
|
||||
if (!required.isEmpty()) {
|
||||
schema.put("required", required);
|
||||
}
|
||||
schema.put("additionalProperties", false);
|
||||
return JsonUtil.toJson(schema);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> parseArguments(String arguments) {
|
||||
if (arguments == null || arguments.isBlank()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
Map<String, Object> inputParams = JsonUtil.fromJson(arguments, Map.class);
|
||||
if (inputParams == null) {
|
||||
throw new IllegalArgumentException("SOP tool arguments must be a valid JSON object");
|
||||
}
|
||||
return inputParams;
|
||||
}
|
||||
|
||||
private Object convertDefaultValue(SopParameter parameter) {
|
||||
String defaultValue = parameter.getDefaultValue();
|
||||
try {
|
||||
return switch (mapType(parameter.getType())) {
|
||||
case "boolean" -> Boolean.valueOf(defaultValue);
|
||||
case "integer" -> Long.valueOf(defaultValue);
|
||||
case "number" -> Double.valueOf(defaultValue);
|
||||
default -> defaultValue;
|
||||
};
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid default value for SOP parameter: " + parameter.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String mapType(String type) {
|
||||
|
||||
+76
-198
@@ -17,31 +17,38 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.registry;
|
||||
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Registry for all @Tool annotated methods.
|
||||
* Automatically discovers and registers tool methods on first use.
|
||||
* Registry for Spring AI tools.
|
||||
*
|
||||
* <p>Scans beans containing {@link Tool} methods on first use and delegates argument deserialization,
|
||||
* proxy method resolution, and result conversion to Spring AI's {@link MethodToolCallbackProvider}.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ToolRegistry {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final Map<String, ToolMethod> tools = new HashMap<>();
|
||||
private volatile Map<String, RegisteredTool> tools = Map.of();
|
||||
private volatile boolean initialized = false;
|
||||
|
||||
@Autowired
|
||||
@@ -49,16 +56,11 @@ public class ToolRegistry {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the registry by scanning all beans for @Tool methods.
|
||||
* Uses double-checked locking for thread safety.
|
||||
*/
|
||||
private void ensureInitialized() {
|
||||
if (!initialized) {
|
||||
synchronized (this) {
|
||||
if (!initialized) {
|
||||
log.info("Scanning for @Tool annotated methods...");
|
||||
scanAllBeans();
|
||||
tools = scanAllBeans();
|
||||
initialized = true;
|
||||
log.info("Registered {} tool methods: {}", tools.size(), tools.keySet());
|
||||
}
|
||||
@@ -66,58 +68,79 @@ public class ToolRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
private void scanAllBeans() {
|
||||
String[] beanNames = applicationContext.getBeanDefinitionNames();
|
||||
for (String beanName : beanNames) {
|
||||
try {
|
||||
Object bean = applicationContext.getBean(beanName);
|
||||
scanBeanForTools(bean);
|
||||
} catch (Exception e) {
|
||||
// Skip beans that cannot be instantiated
|
||||
log.trace("Skipping bean {}: {}", beanName, e.getMessage());
|
||||
}
|
||||
private Map<String, RegisteredTool> scanAllBeans() {
|
||||
log.info("Scanning for @Tool annotated methods...");
|
||||
List<Object> toolBeans = Arrays.stream(applicationContext.getBeanDefinitionNames())
|
||||
.filter(this::containsToolMethod)
|
||||
.map(applicationContext::getBean)
|
||||
.toList();
|
||||
|
||||
if (toolBeans.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
ToolCallback[] callbacks = MethodToolCallbackProvider.builder().toolObjects(toolBeans.toArray())
|
||||
.build().getToolCallbacks();
|
||||
Map<String, RegisteredTool> discoveredTools = new LinkedHashMap<>();
|
||||
for (ToolCallback callback : callbacks) {
|
||||
String toolName = callback.getToolDefinition().name();
|
||||
discoveredTools.put(toolName, new RegisteredTool(callback, getRequiredParameters(callback)));
|
||||
}
|
||||
return Collections.unmodifiableMap(discoveredTools);
|
||||
}
|
||||
|
||||
private void scanBeanForTools(Object bean) {
|
||||
Class<?> clazz = bean.getClass();
|
||||
|
||||
// Handle Spring proxies
|
||||
if (clazz.getName().contains("$$")) {
|
||||
clazz = clazz.getSuperclass();
|
||||
private boolean containsToolMethod(String beanName) {
|
||||
Class<?> beanType = applicationContext.getType(beanName, false);
|
||||
if (beanType == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Method method : clazz.getMethods()) {
|
||||
Tool toolAnnotation = method.getAnnotation(Tool.class);
|
||||
if (toolAnnotation != null) {
|
||||
String toolName = toolAnnotation.name();
|
||||
if (toolName.isEmpty()) {
|
||||
toolName = method.getName();
|
||||
}
|
||||
|
||||
ToolMethod toolMethod = new ToolMethod(bean, method, toolAnnotation);
|
||||
tools.put(toolName, toolMethod);
|
||||
log.debug("Registered tool: {} -> {}.{}",
|
||||
toolName, clazz.getSimpleName(), method.getName());
|
||||
}
|
||||
return Arrays.stream(ReflectionUtils.getDeclaredMethods(ClassUtils.getUserClass(beanType)))
|
||||
.anyMatch(this::isToolMethod);
|
||||
}
|
||||
|
||||
private boolean isToolMethod(Method method) {
|
||||
return AnnotationUtils.findAnnotation(method, Tool.class) != null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Set<String> getRequiredParameters(ToolCallback callback) {
|
||||
Map<String, Object> schema = JsonUtil.fromJson(callback.getToolDefinition().inputSchema(), Map.class);
|
||||
if (schema == null || !(schema.get("required") instanceof List<?> required)) {
|
||||
return Set.of();
|
||||
}
|
||||
return required.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a tool by name with the given arguments.
|
||||
* Invokes a tool with the provided arguments.
|
||||
*/
|
||||
public String invoke(String toolName, Map<String, Object> args) {
|
||||
ensureInitialized();
|
||||
ToolMethod toolMethod = tools.get(toolName);
|
||||
if (toolMethod == null) {
|
||||
throw new IllegalArgumentException("Unknown tool: " + toolName
|
||||
RegisteredTool tool = tools.get(toolName);
|
||||
if (tool == null) {
|
||||
throw new IllegalArgumentException("Unknown tool: " + toolName
|
||||
+ ". Available tools: " + tools.keySet());
|
||||
}
|
||||
return toolMethod.invoke(args);
|
||||
Map<String, Object> safeArgs = args == null ? Map.of() : args;
|
||||
validateRequiredParameters(tool.requiredParameters(), safeArgs);
|
||||
String result = tool.callback().call(JsonUtil.toJson(safeArgs));
|
||||
Object convertedResult = JsonUtil.fromJson(result, Object.class);
|
||||
return convertedResult instanceof String text ? text : result;
|
||||
}
|
||||
|
||||
private void validateRequiredParameters(Set<String> requiredParameters, Map<String, Object> args) {
|
||||
for (String parameter : requiredParameters) {
|
||||
Object value = args.get(parameter);
|
||||
if (value == null || value instanceof String text && text.isBlank()) {
|
||||
throw new IllegalArgumentException("Required tool parameter is missing: " + parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool exists.
|
||||
* Checks whether a tool exists.
|
||||
*/
|
||||
public boolean hasMethod(String toolName) {
|
||||
ensureInitialized();
|
||||
@@ -125,158 +148,13 @@ public class ToolRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tool names.
|
||||
* Returns an immutable snapshot of all registered tool names.
|
||||
*/
|
||||
public Set<String> getToolNames() {
|
||||
ensureInitialized();
|
||||
return tools.keySet();
|
||||
return Set.copyOf(tools.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool method info.
|
||||
*/
|
||||
public ToolMethod getToolMethod(String toolName) {
|
||||
ensureInitialized();
|
||||
return tools.get(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a registered tool method.
|
||||
*/
|
||||
public static class ToolMethod {
|
||||
private final Object bean;
|
||||
private final Method method;
|
||||
private final Tool annotation;
|
||||
private final List<ParamInfo> paramInfos;
|
||||
|
||||
public ToolMethod(Object bean, Method method, Tool annotation) {
|
||||
this.bean = bean;
|
||||
this.method = method;
|
||||
this.annotation = annotation;
|
||||
this.paramInfos = extractParamInfos(method);
|
||||
}
|
||||
|
||||
private List<ParamInfo> extractParamInfos(Method method) {
|
||||
List<ParamInfo> infos = new ArrayList<>();
|
||||
Parameter[] parameters = method.getParameters();
|
||||
|
||||
for (Parameter param : parameters) {
|
||||
ToolParam toolParam = param.getAnnotation(ToolParam.class);
|
||||
String name = (toolParam != null && !toolParam.description().isEmpty())
|
||||
? param.getName() : param.getName();
|
||||
boolean required = toolParam != null && toolParam.required();
|
||||
|
||||
infos.add(new ParamInfo(name, param.getType(), required));
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke this tool method with the given arguments.
|
||||
*/
|
||||
public String invoke(Map<String, Object> args) {
|
||||
try {
|
||||
Object[] methodArgs = new Object[paramInfos.size()];
|
||||
|
||||
for (int i = 0; i < paramInfos.size(); i++) {
|
||||
ParamInfo paramInfo = paramInfos.get(i);
|
||||
Object value = args.get(paramInfo.name);
|
||||
methodArgs[i] = convertValue(value, paramInfo.type);
|
||||
}
|
||||
|
||||
Object result = method.invoke(bean, methodArgs);
|
||||
return result != null ? result.toString() : "";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke tool {}: {}", annotation.name(), e.getMessage(), e);
|
||||
throw new RuntimeException("Tool invocation failed: " + annotation.name(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private Object convertValue(Object value, Class<?> targetType) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (targetType.isAssignableFrom(value.getClass())) {
|
||||
return value;
|
||||
}
|
||||
|
||||
String strValue = String.valueOf(value);
|
||||
|
||||
if (targetType == String.class) {
|
||||
return strValue;
|
||||
} else if (targetType == Integer.class || targetType == int.class) {
|
||||
return strValue.isEmpty() ? null : Integer.valueOf(strValue);
|
||||
} else if (targetType == Long.class || targetType == long.class) {
|
||||
return strValue.isEmpty() ? null : Long.valueOf(strValue);
|
||||
} else if (targetType == Boolean.class || targetType == boolean.class) {
|
||||
return Boolean.valueOf(strValue);
|
||||
} else if (targetType == Byte.class || targetType == byte.class) {
|
||||
return strValue.isEmpty() ? null : Byte.valueOf(strValue);
|
||||
} else if (targetType == Double.class || targetType == double.class) {
|
||||
return strValue.isEmpty() ? null : Double.valueOf(strValue);
|
||||
} else if (targetType == Float.class || targetType == float.class) {
|
||||
return strValue.isEmpty() ? null : Float.valueOf(strValue);
|
||||
} else if (targetType == List.class) {
|
||||
return parseList(strValue);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Long> parseList(String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Long> result = new ArrayList<>();
|
||||
for (String s : value.split(",")) {
|
||||
if (!s.trim().isEmpty()) {
|
||||
result.add(Long.valueOf(s.trim()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return annotation.name();
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return annotation.description();
|
||||
}
|
||||
|
||||
public List<ParamInfo> getParamInfos() {
|
||||
return paramInfos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter information for a tool method.
|
||||
*/
|
||||
public static class ParamInfo {
|
||||
private final String name;
|
||||
private final Class<?> type;
|
||||
private final boolean required;
|
||||
|
||||
public ParamInfo(String name, Class<?> type, boolean required) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
private record RegisteredTool(ToolCallback callback, Set<String> requiredParameters) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,23 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# 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.
|
||||
|
||||
# SOP Result Messages - Default (Chinese)
|
||||
sop.result.title=SOP执行结果
|
||||
sop.result.name=SOP名称
|
||||
|
||||
@@ -13,23 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# 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.
|
||||
|
||||
# SOP Result Messages - English
|
||||
sop.result.title=SOP Execution Result
|
||||
sop.result.name=SOP Name
|
||||
|
||||
@@ -13,23 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# 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.
|
||||
|
||||
# SOP Result Messages - Chinese
|
||||
sop.result.title=SOP执行结果
|
||||
sop.result.name=SOP名称
|
||||
|
||||
@@ -65,12 +65,16 @@ POST /api/ai/sop/execute/{skillName}/ai
|
||||
|
||||
```
|
||||
YAML 定义 → SkillRegistry → SopEngine → Executors → SopResult
|
||||
↓
|
||||
ToolExecutor / LlmExecutor
|
||||
↓ ↓
|
||||
SopToolCallback ToolExecutor / LlmExecutor
|
||||
↓
|
||||
ToolRegistry(自动发现 @Tool 方法)
|
||||
```
|
||||
|
||||
所有执行入口都由 `SopEngine` 统一补齐默认参数并校验必填参数。技能注册为 Spring AI
|
||||
工具时,`SopToolCallback` 会根据参数定义生成 JSON Schema、解析模型传入的 JSON 对象,
|
||||
同步执行 SOP 并返回 `SopResult`;调用方不应在控制器或工具外层重复实现参数校验。
|
||||
|
||||
## 添加新工具
|
||||
|
||||
只需添加 `@Tool` 注解,无需修改其他代码:
|
||||
|
||||
@@ -13,23 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# 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: daily_inspection
|
||||
description: "Execute daily health inspection on all monitors and generate a comprehensive report"
|
||||
version: "1.0"
|
||||
|
||||
@@ -13,23 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# 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: mysql_slow_query_diagnosis
|
||||
description: "Diagnose MySQL slow queries and provide optimization recommendations"
|
||||
version: "1.0"
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.config;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
|
||||
/**
|
||||
* Verifies the security context scope used when model tools execute across threads.
|
||||
*/
|
||||
class McpContextHolderTest {
|
||||
|
||||
@AfterEach
|
||||
void clearContext() {
|
||||
McpContextHolder.clear();
|
||||
SurenessContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void callWithSubjectShouldBindAndClearBothContexts() {
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
|
||||
String result = McpContextHolder.callWithSubject(subject, () -> {
|
||||
assertSame(subject, McpContextHolder.getSubject());
|
||||
assertSame(subject, SurenessContextHolder.getBindSubject());
|
||||
return "result";
|
||||
});
|
||||
|
||||
assertEquals("result", result);
|
||||
assertNull(McpContextHolder.getSubject());
|
||||
assertNull(SurenessContextHolder.getBindSubject());
|
||||
}
|
||||
|
||||
@Test
|
||||
void callWithSubjectShouldRestoreIndependentPreviousContexts() {
|
||||
SubjectSum previousMcpSubject = mock(SubjectSum.class);
|
||||
SubjectSum previousSurenessSubject = mock(SubjectSum.class);
|
||||
SubjectSum currentSubject = mock(SubjectSum.class);
|
||||
McpContextHolder.setSubject(previousMcpSubject);
|
||||
SurenessContextHolder.bindSubject(previousSurenessSubject);
|
||||
|
||||
McpContextHolder.callWithSubject(currentSubject, () -> {
|
||||
assertSame(currentSubject, McpContextHolder.getSubject());
|
||||
assertSame(currentSubject, SurenessContextHolder.getBindSubject());
|
||||
return null;
|
||||
});
|
||||
|
||||
assertSame(previousMcpSubject, McpContextHolder.getSubject());
|
||||
assertSame(previousSurenessSubject, SurenessContextHolder.getBindSubject());
|
||||
}
|
||||
|
||||
@Test
|
||||
void callWithSubjectShouldRestoreContextsAfterFailure() {
|
||||
SubjectSum previousSubject = mock(SubjectSum.class);
|
||||
SubjectSum currentSubject = mock(SubjectSum.class);
|
||||
McpContextHolder.setSubject(previousSubject);
|
||||
SurenessContextHolder.bindSubject(previousSubject);
|
||||
|
||||
assertThrows(IllegalStateException.class, () ->
|
||||
McpContextHolder.callWithSubject(currentSubject, () -> {
|
||||
throw new IllegalStateException("tool failed");
|
||||
}));
|
||||
|
||||
assertSame(previousSubject, McpContextHolder.getSubject());
|
||||
assertSame(previousSubject, SurenessContextHolder.getBindSubject());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toolContextShouldCarryOnlyValidSubject() {
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
ToolContext toolContext = new ToolContext(McpContextHolder.createToolContext(subject));
|
||||
|
||||
assertSame(subject, McpContextHolder.getSubject(toolContext));
|
||||
assertNull(McpContextHolder.getSubject(null));
|
||||
assertNull(McpContextHolder.getSubject(new ToolContext(
|
||||
java.util.Map.of(McpContextHolder.SUBJECT_CONTEXT_KEY, "invalid"))));
|
||||
assertEquals(java.util.Map.of(), McpContextHolder.createToolContext(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callbackShouldExposeSubjectOnlyDuringDelegateCall() {
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
ToolContext toolContext = new ToolContext(McpContextHolder.createToolContext(subject));
|
||||
ToolCallback delegate = new ToolCallback() {
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return ToolDefinition.builder()
|
||||
.name("test")
|
||||
.description("test")
|
||||
.inputSchema("{}")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String input, ToolContext context) {
|
||||
if (context == null) {
|
||||
return input;
|
||||
}
|
||||
assertSame(subject, McpContextHolder.getSubject());
|
||||
assertSame(subject, SurenessContextHolder.getBindSubject());
|
||||
return "ok";
|
||||
}
|
||||
};
|
||||
SecurityContextToolCallback callback = new SecurityContextToolCallback(delegate);
|
||||
|
||||
assertEquals("test", callback.getToolDefinition().name());
|
||||
assertEquals(delegate.getToolMetadata().returnDirect(), callback.getToolMetadata().returnDirect());
|
||||
assertEquals("plain", callback.call("plain"));
|
||||
String result = callback.call("{}", toolContext);
|
||||
|
||||
assertEquals("ok", result);
|
||||
assertNull(McpContextHolder.getSubject());
|
||||
assertNull(SurenessContextHolder.getBindSubject());
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.config.SecurityContextToolCallback;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
|
||||
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
|
||||
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
|
||||
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
|
||||
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Verifies that the provider configuration cache reacts to enable and disable events.
|
||||
*/
|
||||
class ChatClientProviderServiceImplTest {
|
||||
|
||||
@Test
|
||||
void configurationChangeShouldRefreshConfiguredState() {
|
||||
AtomicReference<GeneralConfig> currentConfig = new AtomicReference<>();
|
||||
GeneralConfigDao configDao = configDao(currentConfig);
|
||||
ChatClientProviderServiceImpl service = new ChatClientProviderServiceImpl(null, configDao, null);
|
||||
|
||||
assertFalse(service.isConfigured());
|
||||
|
||||
currentConfig.set(providerConfig("sk-test"));
|
||||
service.onAiProviderConfigChange(changeEvent());
|
||||
assertTrue(service.isConfigured());
|
||||
|
||||
currentConfig.set(providerConfig(" "));
|
||||
service.onAiProviderConfigChange(changeEvent());
|
||||
assertFalse(service.isConfigured());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void streamChatShouldAttachSubjectAndWrapEveryToolCallback() {
|
||||
ApplicationContext applicationContext = mock(ApplicationContext.class);
|
||||
SkillRegistry skillRegistry = mock(SkillRegistry.class);
|
||||
ChatClient chatClient = mock(ChatClient.class);
|
||||
ChatClient.ChatClientRequestSpec requestSpec = mock(ChatClient.ChatClientRequestSpec.class);
|
||||
ChatClient.StreamResponseSpec streamSpec = mock(ChatClient.StreamResponseSpec.class);
|
||||
ToolCallback delegate = mock(ToolCallback.class);
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
ChatClientProviderServiceImpl service = new ChatClientProviderServiceImpl(
|
||||
applicationContext, configDao(new AtomicReference<>()), skillRegistry);
|
||||
|
||||
when(applicationContext.getBean("openAiChatClient", ChatClient.class)).thenReturn(chatClient);
|
||||
when(chatClient.prompt()).thenReturn(requestSpec);
|
||||
when(requestSpec.messages(anyList())).thenReturn(requestSpec);
|
||||
when(requestSpec.system(anyString())).thenReturn(requestSpec);
|
||||
when(requestSpec.tools(any(Object[].class))).thenReturn(requestSpec);
|
||||
when(requestSpec.toolContext(anyMap())).thenReturn(requestSpec);
|
||||
when(requestSpec.stream()).thenReturn(streamSpec);
|
||||
when(streamSpec.content()).thenReturn(Flux.just("answer"));
|
||||
when(skillRegistry.getAllSkills()).thenReturn(java.util.List.of());
|
||||
ReflectionTestUtils.setField(service, "systemResource", new ByteArrayResource(
|
||||
"skills={dynamically_injected_skills_list}; conversation={current_conversation_id}"
|
||||
.getBytes(StandardCharsets.UTF_8)));
|
||||
ReflectionTestUtils.setField(service, "toolCallbackProvider", ToolCallbackProvider.from(delegate));
|
||||
|
||||
ChatRequestContext context = ChatRequestContext.builder()
|
||||
.message("question")
|
||||
.conversationId(42L)
|
||||
.subject(subject)
|
||||
.build();
|
||||
service.streamChat(context).collectList().block();
|
||||
|
||||
ArgumentCaptor<Object[]> callbacksCaptor = ArgumentCaptor.forClass(Object[].class);
|
||||
ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class);
|
||||
verify(requestSpec).tools(callbacksCaptor.capture());
|
||||
verify(requestSpec).toolContext(contextCaptor.capture());
|
||||
assertInstanceOf(SecurityContextToolCallback.class, callbacksCaptor.getValue()[0]);
|
||||
assertSame(subject, McpContextHolder.getSubject(new org.springframework.ai.chat.model.ToolContext(
|
||||
contextCaptor.getValue())));
|
||||
}
|
||||
|
||||
private GeneralConfigDao configDao(AtomicReference<GeneralConfig> currentConfig) {
|
||||
return (GeneralConfigDao) Proxy.newProxyInstance(
|
||||
GeneralConfigDao.class.getClassLoader(),
|
||||
new Class<?>[]{GeneralConfigDao.class},
|
||||
(proxy, method, args) -> "findByType".equals(method.getName()) ? currentConfig.get() : null);
|
||||
}
|
||||
|
||||
private GeneralConfig providerConfig(String apiKey) {
|
||||
ModelProviderConfig modelConfig = new ModelProviderConfig();
|
||||
modelConfig.setApiKey(apiKey);
|
||||
return GeneralConfig.builder()
|
||||
.type("provider")
|
||||
.content(JsonUtil.toJson(modelConfig))
|
||||
.build();
|
||||
}
|
||||
|
||||
private AiProviderConfigChangeEvent changeEvent() {
|
||||
return new AiProviderConfigChangeEvent(new StaticApplicationContext());
|
||||
}
|
||||
}
|
||||
+11
@@ -23,6 +23,8 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
@@ -34,6 +36,7 @@ import org.apache.hertzbeat.ai.service.ChatClientProviderService;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -62,8 +65,15 @@ class ConversationServiceImplTest {
|
||||
@InjectMocks
|
||||
private ConversationServiceImpl conversationService;
|
||||
|
||||
@AfterEach
|
||||
void clearSecurityContext() {
|
||||
SurenessContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamChatShouldKeepCompleteConversationHistory() {
|
||||
SubjectSum subject = org.mockito.Mockito.mock(SubjectSum.class);
|
||||
SurenessContextHolder.bindSubject(subject);
|
||||
ChatConversation conversation = ChatConversation.builder()
|
||||
.id(CONVERSATION_ID)
|
||||
.title("已命名会话")
|
||||
@@ -104,5 +114,6 @@ class ConversationServiceImplTest {
|
||||
ArgumentCaptor<ChatRequestContext> contextCaptor = ArgumentCaptor.forClass(ChatRequestContext.class);
|
||||
verify(chatClientProviderService).streamChat(contextCaptor.capture());
|
||||
assertEquals(history, contextCaptor.getValue().getConversationHistory());
|
||||
assertEquals(subject, contextCaptor.getValue().getSubject());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.sop.engine;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.hertzbeat.ai.sop.executor.SopExecutor;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopStep;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Verifies that all SOP execution entry points follow the same parameter contract.
|
||||
*/
|
||||
class SopEngineImplTest {
|
||||
|
||||
@Test
|
||||
void executeSyncShouldReturnFailureForNullDefinition() {
|
||||
SopEngineImpl engine = new SopEngineImpl(List.of());
|
||||
|
||||
SopResult result = engine.executeSync(null, Map.of());
|
||||
|
||||
assertEquals("FAILED", result.getStatus());
|
||||
assertTrue(result.getError().contains("must not be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeSyncShouldRejectMissingRequiredParameter() {
|
||||
SopEngineImpl engine = new SopEngineImpl(List.of(new RecordingExecutor()));
|
||||
|
||||
SopResult result = engine.executeSync(definition(requiredParameter()), Map.of());
|
||||
|
||||
assertEquals("FAILED", result.getStatus());
|
||||
assertTrue(result.getError().contains("monitorId"));
|
||||
assertTrue(result.getSteps().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeSyncShouldApplyDefaultParameter() {
|
||||
AtomicReference<Map<String, Object>> capturedContext = new AtomicReference<>();
|
||||
SopEngineImpl engine = new SopEngineImpl(List.of(new RecordingExecutor(capturedContext)));
|
||||
|
||||
SopResult result = engine.executeSync(definition(defaultParameter()), Map.of());
|
||||
|
||||
assertEquals("SUCCESS", result.getStatus());
|
||||
assertEquals("10", capturedContext.get().get("limit"));
|
||||
assertEquals("zh", capturedContext.get().get("_language"));
|
||||
assertEquals("10", result.getData().get("limit"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeStreamShouldApplyTheSameParameterRules() {
|
||||
AtomicReference<Map<String, Object>> capturedContext = new AtomicReference<>();
|
||||
SopEngineImpl engine = new SopEngineImpl(List.of(new RecordingExecutor(capturedContext)));
|
||||
|
||||
List<String> events = engine.execute(definition(defaultParameter()), null).collectList().block();
|
||||
|
||||
assertNotNull(events);
|
||||
assertTrue(events.getLast().contains("completed successfully"));
|
||||
assertEquals("10", capturedContext.get().get("limit"));
|
||||
}
|
||||
|
||||
private SopDefinition definition(SopParameter parameter) {
|
||||
return SopDefinition.builder()
|
||||
.name("test-sop")
|
||||
.version("1.0")
|
||||
.parameters(List.of(parameter))
|
||||
.steps(List.of(SopStep.builder().id("query").type("tool").build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private SopParameter requiredParameter() {
|
||||
return SopParameter.builder().name("monitorId").type("long").required(true).build();
|
||||
}
|
||||
|
||||
private SopParameter defaultParameter() {
|
||||
return SopParameter.builder().name("limit").type("integer").defaultValue("10").build();
|
||||
}
|
||||
|
||||
private static final class RecordingExecutor implements SopExecutor {
|
||||
|
||||
private final AtomicReference<Map<String, Object>> capturedContext;
|
||||
|
||||
private RecordingExecutor() {
|
||||
this(new AtomicReference<>());
|
||||
}
|
||||
|
||||
private RecordingExecutor(AtomicReference<Map<String, Object>> capturedContext) {
|
||||
this.capturedContext = capturedContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(String type) {
|
||||
return "tool".equals(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object execute(SopStep step, Map<String, Object> context) {
|
||||
capturedContext.set(Map.copyOf(context));
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.sop.registry;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.hertzbeat.ai.sop.engine.SopEngine;
|
||||
import org.apache.hertzbeat.ai.sop.model.OutputType;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Verifies the input contract and execution behavior of a SOP exposed as a Spring AI tool.
|
||||
*/
|
||||
class SopToolCallbackTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void schemaShouldDeclareRequiredParametersAndEscapeDescriptions() {
|
||||
SopDefinition definition = definition();
|
||||
SopToolCallback callback = new SopToolCallback(definition, new RecordingEngine());
|
||||
|
||||
Map<String, Object> schema = JsonUtil.fromJson(callback.getToolDefinition().inputSchema(), Map.class);
|
||||
Map<String, Object> properties = (Map<String, Object>) schema.get("properties");
|
||||
Map<String, Object> monitorId = (Map<String, Object>) properties.get("monitorId");
|
||||
Map<String, Object> limit = (Map<String, Object>) properties.get("limit");
|
||||
|
||||
assertEquals(List.of("monitorId"), schema.get("required"));
|
||||
assertEquals(false, schema.get("additionalProperties"));
|
||||
assertEquals("integer", monitorId.get("type"));
|
||||
assertEquals("包含 \"引号\" 的说明", monitorId.get("description"));
|
||||
assertEquals(10, limit.get("default"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callShouldParseArgumentsAndExecuteSop() {
|
||||
RecordingEngine engine = new RecordingEngine();
|
||||
SopToolCallback callback = new SopToolCallback(definition(), engine);
|
||||
|
||||
String response = callback.call("{\"monitorId\":42}");
|
||||
|
||||
assertEquals(42, engine.inputParams.get().get("monitorId"));
|
||||
assertTrue(response.contains("SUCCESS"));
|
||||
assertFalse(response.contains("execution started"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callShouldRejectInvalidJson() {
|
||||
SopToolCallback callback = new SopToolCallback(definition(), new RecordingEngine());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> callback.call("not-json"));
|
||||
}
|
||||
|
||||
private SopDefinition definition() {
|
||||
SopParameter monitorId = SopParameter.builder()
|
||||
.name("monitorId")
|
||||
.type("long")
|
||||
.description("包含 \"引号\" 的说明")
|
||||
.required(true)
|
||||
.build();
|
||||
SopParameter limit = SopParameter.builder()
|
||||
.name("limit")
|
||||
.type("integer")
|
||||
.defaultValue("10")
|
||||
.build();
|
||||
return SopDefinition.builder()
|
||||
.name("diagnose")
|
||||
.description("诊断工具")
|
||||
.version("1.0")
|
||||
.parameters(List.of(monitorId, limit))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static final class RecordingEngine implements SopEngine {
|
||||
|
||||
private final AtomicReference<Map<String, Object>> inputParams = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public Flux<String> execute(SopDefinition definition, Map<String, Object> inputParams) {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SopResult executeSync(SopDefinition definition, Map<String, Object> inputParams) {
|
||||
this.inputParams.set(inputParams);
|
||||
return SopResult.builder()
|
||||
.sopName(definition.getName())
|
||||
.sopVersion(definition.getVersion())
|
||||
.status("SUCCESS")
|
||||
.outputType(OutputType.SIMPLE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.sop.registry;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionException;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
/**
|
||||
* Verifies tool discovery, argument validation, and type conversion by Spring AI callbacks.
|
||||
*/
|
||||
class ToolRegistryTest {
|
||||
|
||||
@Test
|
||||
void invokeShouldConvertStructuredArguments() {
|
||||
try (GenericApplicationContext context = contextWith(ToolFixture.class)) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
String result = registry.invoke("inspect", Map.of(
|
||||
"monitorId", 42,
|
||||
"ids", List.of(1, 2),
|
||||
"enabled", true));
|
||||
|
||||
assertEquals("Long|Long|Boolean", result);
|
||||
assertTrue(registry.hasMethod("inspect"));
|
||||
assertEquals(Set.of("inspect", "optional"), registry.getToolNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeShouldRejectMissingRequiredParameter() {
|
||||
try (GenericApplicationContext context = contextWith(ToolFixture.class)) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
|
||||
() -> registry.invoke("inspect", Map.of("ids", List.of(1), "enabled", true)));
|
||||
|
||||
assertEquals("Required tool parameter is missing: monitorId", error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeShouldRejectInvalidBooleanInsteadOfSilentlyUsingFalse() {
|
||||
try (GenericApplicationContext context = contextWith(ToolFixture.class)) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
assertThrows(ToolExecutionException.class, () -> registry.invoke("inspect", Map.of(
|
||||
"monitorId", 42,
|
||||
"ids", List.of(1),
|
||||
"enabled", "not-a-boolean")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void initializationShouldRejectDuplicateToolNames() {
|
||||
try (GenericApplicationContext context = contextWith(ToolFixture.class, DuplicateToolFixture.class)) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
IllegalArgumentException error = assertThrows(
|
||||
IllegalArgumentException.class, registry::getToolNames);
|
||||
|
||||
assertTrue(error.getMessage().contains("inspect"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyRegistryShouldReportUnknownTool() {
|
||||
try (GenericApplicationContext context = contextWith()) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
assertTrue(registry.getToolNames().isEmpty());
|
||||
assertThrows(IllegalArgumentException.class, () -> registry.invoke("missing", null));
|
||||
}
|
||||
}
|
||||
|
||||
private GenericApplicationContext contextWith(Class<?>... beanTypes) {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
for (Class<?> beanType : beanTypes) {
|
||||
context.registerBean(beanType);
|
||||
}
|
||||
context.refresh();
|
||||
return context;
|
||||
}
|
||||
|
||||
public static final class ToolFixture {
|
||||
|
||||
@Tool(name = "inspect", description = "Inspect argument types")
|
||||
public String inspect(
|
||||
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
|
||||
@ToolParam(description = "Monitor ID list", required = false) List<Long> ids,
|
||||
@ToolParam(description = "Whether enabled", required = true) Boolean enabled) {
|
||||
return "%s|%s|%s".formatted(
|
||||
monitorId.getClass().getSimpleName(),
|
||||
ids.getFirst().getClass().getSimpleName(),
|
||||
enabled.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Tool(name = "optional", description = "Tool without arguments")
|
||||
public String optional() {
|
||||
return "optional";
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DuplicateToolFixture {
|
||||
|
||||
@Tool(name = "inspect", description = "Duplicate-name test tool")
|
||||
public String inspect() {
|
||||
return "duplicate";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+6
-3
@@ -30,6 +30,7 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.alert.service.NoticeConfigService;
|
||||
import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -87,14 +88,16 @@ public class NoticeConfigController {
|
||||
@Parameter(description = "en: Recipient name,support fuzzy query", example = "tom") @RequestParam(required = false) final String name,
|
||||
@Parameter(description = "en: List current page", example = "0") @RequestParam(defaultValue = "0") final int pageIndex,
|
||||
@Parameter(description = "en: Number of list pages", example = "8") @RequestParam(defaultValue = "8") final int pageSize) {
|
||||
return ResponseEntity.ok(Message.success(noticeConfigService.getNoticeReceivers(name, pageIndex, pageSize)));
|
||||
return ResponseEntity.ok(Message.success(noticeConfigService.getNoticeReceivers(name, pageIndex, pageSize)
|
||||
.map(NoticeReceiverMaskUtil::mask)));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/receivers/all")
|
||||
@Operation(summary = "Get a list of all message notification recipients",
|
||||
description = "Get a list of all message notification recipients")
|
||||
public ResponseEntity<Message<List<NoticeReceiver>>> getAllReceivers() {
|
||||
return ResponseEntity.ok(Message.success(noticeConfigService.getAllNoticeReceivers()));
|
||||
return ResponseEntity.ok(Message.success(noticeConfigService.getAllNoticeReceivers().stream()
|
||||
.map(NoticeReceiverMaskUtil::mask).toList()));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/receiver/{id}")
|
||||
@@ -106,7 +109,7 @@ public class NoticeConfigController {
|
||||
if (noticeReceiver == null) {
|
||||
return ResponseEntity.ok(Message.fail(FAIL_CODE, "The relevant information of the recipient could not be found, please check whether the parameters are correct or refresh the page"));
|
||||
}
|
||||
return ResponseEntity.ok(Message.success(noticeReceiver));
|
||||
return ResponseEntity.ok(Message.success(NoticeReceiverMaskUtil.mask(noticeReceiver)));
|
||||
}
|
||||
|
||||
@PostMapping(path = "/rule")
|
||||
|
||||
+21
@@ -31,6 +31,7 @@ import org.apache.hertzbeat.alert.dao.NoticeReceiverDao;
|
||||
import org.apache.hertzbeat.alert.dao.NoticeRuleDao;
|
||||
import org.apache.hertzbeat.alert.dao.NoticeTemplateDao;
|
||||
import org.apache.hertzbeat.alert.service.NoticeConfigService;
|
||||
import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
@@ -176,9 +177,28 @@ public class NoticeConfigServiceImpl implements NoticeConfigService, CommandLine
|
||||
|
||||
@Override
|
||||
public void editReceiver(NoticeReceiver noticeReceiver) {
|
||||
resolveMaskedSecrets(noticeReceiver);
|
||||
noticeReceiverDao.save(noticeReceiver);
|
||||
}
|
||||
|
||||
/**
|
||||
* The rest api returns receivers with masked secret fields, so a receiver submitted
|
||||
* from the ui may carry the mask placeholder instead of the real secret.
|
||||
* Restore such fields from the stored entity before using the receiver.
|
||||
* @throws IllegalArgumentException if the receiver carries an id but no stored receiver
|
||||
* exists for it: without the stored entity the mask cannot
|
||||
* be resolved, and saving would persist the placeholder
|
||||
*/
|
||||
private void resolveMaskedSecrets(NoticeReceiver noticeReceiver) {
|
||||
if (noticeReceiver == null || noticeReceiver.getId() == null) {
|
||||
return;
|
||||
}
|
||||
NoticeReceiver existing = noticeReceiverDao.findById(noticeReceiver.getId())
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"The receiver with id " + noticeReceiver.getId() + " does not exist."));
|
||||
NoticeReceiverMaskUtil.resolveMask(noticeReceiver, existing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteReceiver(Long receiverId) {
|
||||
noticeReceiverDao.deleteById(receiverId);
|
||||
@@ -319,6 +339,7 @@ public class NoticeConfigServiceImpl implements NoticeConfigService, CommandLine
|
||||
|
||||
@Override
|
||||
public boolean sendTestMsg(NoticeReceiver noticeReceiver) {
|
||||
resolveMaskedSecrets(noticeReceiver);
|
||||
Map<String, String> labels = new HashMap<>(8);
|
||||
labels.put(CommonConstants.LABEL_INSTANCE, "127.0.0.1");
|
||||
labels.put(CommonConstants.LABEL_ALERT_NAME, "CPU Usage Alert");
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.alert.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
|
||||
/**
|
||||
* Masks the secret fields of {@link NoticeReceiver} before it is exposed through the rest api,
|
||||
* and resolves masked values back to the stored secrets when an edited receiver is submitted.
|
||||
* A secret long enough keeps its last characters visible so different tokens stay
|
||||
* distinguishable in the ui, while an unchanged secret round-trips through the ui
|
||||
* without ever leaving the server.
|
||||
*/
|
||||
public final class NoticeReceiverMaskUtil {
|
||||
|
||||
/**
|
||||
* Fixed-length placeholder replacing the hidden part of a secret. Its length is
|
||||
* constant on purpose so the mask never reveals how long the real secret is.
|
||||
*/
|
||||
public static final String SECRET_MASK = "******";
|
||||
|
||||
/**
|
||||
* A secret shorter than this is masked entirely: revealing a suffix of a short
|
||||
* secret would give away too large a fraction of it.
|
||||
*/
|
||||
private static final int MIN_LENGTH_TO_SHOW_SUFFIX = 12;
|
||||
|
||||
/**
|
||||
* Number of trailing characters kept visible for a long secret,
|
||||
* enough to tell configured tokens apart.
|
||||
*/
|
||||
private static final int VISIBLE_SUFFIX_LENGTH = 4;
|
||||
|
||||
private record SecretField(Function<NoticeReceiver, String> getter, BiConsumer<NoticeReceiver, String> setter) {
|
||||
}
|
||||
|
||||
private static final List<SecretField> SECRET_FIELDS = List.of(
|
||||
new SecretField(NoticeReceiver::getHookAuthToken, NoticeReceiver::setHookAuthToken),
|
||||
new SecretField(NoticeReceiver::getAccessToken, NoticeReceiver::setAccessToken),
|
||||
new SecretField(NoticeReceiver::getTgBotToken, NoticeReceiver::setTgBotToken),
|
||||
new SecretField(NoticeReceiver::getSlackWebHookUrl, NoticeReceiver::setSlackWebHookUrl),
|
||||
new SecretField(NoticeReceiver::getAppSecret, NoticeReceiver::setAppSecret),
|
||||
new SecretField(NoticeReceiver::getDiscordBotToken, NoticeReceiver::setDiscordBotToken),
|
||||
new SecretField(NoticeReceiver::getSmnAk, NoticeReceiver::setSmnAk),
|
||||
new SecretField(NoticeReceiver::getSmnSk, NoticeReceiver::setSmnSk),
|
||||
new SecretField(NoticeReceiver::getServerChanToken, NoticeReceiver::setServerChanToken),
|
||||
new SecretField(NoticeReceiver::getGotifyToken, NoticeReceiver::setGotifyToken),
|
||||
new SecretField(NoticeReceiver::getNtfyToken, NoticeReceiver::setNtfyToken));
|
||||
|
||||
private NoticeReceiverMaskUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a copy of the receiver with all secret fields masked.
|
||||
* The given entity is not modified as it may still be attached to the persistence context.
|
||||
* @param receiver receiver to mask, may be null
|
||||
* @return masked copy, or null if receiver is null
|
||||
*/
|
||||
public static NoticeReceiver mask(NoticeReceiver receiver) {
|
||||
if (receiver == null) {
|
||||
return null;
|
||||
}
|
||||
NoticeReceiver masked = receiver.toBuilder().build();
|
||||
for (SecretField field : SECRET_FIELDS) {
|
||||
field.setter().accept(masked, maskValue(field.getter().apply(masked)));
|
||||
}
|
||||
return masked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace every secret field of the incoming receiver that still holds the masked form
|
||||
* of the stored secret with the stored value, so an edit that did not touch a secret keeps it.
|
||||
* A re-entered secret or a cleared field is left untouched.
|
||||
* @param incoming receiver submitted by the ui, modified in place
|
||||
* @param existing receiver currently stored in the database
|
||||
*/
|
||||
public static void resolveMask(NoticeReceiver incoming, NoticeReceiver existing) {
|
||||
if (incoming == null || existing == null) {
|
||||
return;
|
||||
}
|
||||
for (SecretField field : SECRET_FIELDS) {
|
||||
String submitted = field.getter().apply(incoming);
|
||||
String stored = field.getter().apply(existing);
|
||||
if (isMaskOf(submitted, stored)) {
|
||||
field.setter().accept(incoming, stored);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMaskOf(String submitted, String stored) {
|
||||
if (submitted == null || StringUtils.isBlank(stored)) {
|
||||
return false;
|
||||
}
|
||||
return submitted.equals(SECRET_MASK) || submitted.equals(maskValue(stored));
|
||||
}
|
||||
|
||||
private static String maskValue(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
if (value.length() < MIN_LENGTH_TO_SHOW_SUFFIX) {
|
||||
return SECRET_MASK;
|
||||
}
|
||||
return SECRET_MASK + value.substring(value.length() - VISIBLE_SUFFIX_LENGTH);
|
||||
}
|
||||
}
|
||||
+5
@@ -31,6 +31,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl;
|
||||
import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
|
||||
@@ -174,6 +175,7 @@ class NoticeConfigControllerTest {
|
||||
NoticeReceiver receiver1 = new NoticeReceiver();
|
||||
receiver1.setId(1L);
|
||||
receiver1.setName("Receiver1");
|
||||
receiver1.setTgBotToken("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM");
|
||||
|
||||
NoticeReceiver receiver2 = new NoticeReceiver();
|
||||
receiver2.setId(2L);
|
||||
@@ -197,6 +199,7 @@ class NoticeConfigControllerTest {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.content[0].id").value(1))
|
||||
.andExpect(jsonPath("$.data.content[0].name").value("Receiver1"))
|
||||
.andExpect(jsonPath("$.data.content[0].tgBotToken").value(NoticeReceiverMaskUtil.SECRET_MASK + "voJM"))
|
||||
.andExpect(jsonPath("$.data.content[1].id").value(2))
|
||||
.andExpect(jsonPath("$.data.content[1].name").value("Receiver2"))
|
||||
.andExpect(jsonPath("$.data.totalElements").value(2))
|
||||
@@ -222,6 +225,8 @@ class NoticeConfigControllerTest {
|
||||
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/receiver/{id}", 7565463543L))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.data.accessToken").value(NoticeReceiverMaskUtil.SECRET_MASK + "739d"))
|
||||
.andExpect(jsonPath("$.data.email").value("2762242004@qq.com"))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
|
||||
-19
@@ -17,25 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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.alert.reduce;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
-19
@@ -17,25 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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.alert.reduce;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
+65
-2
@@ -22,6 +22,7 @@ import org.apache.hertzbeat.alert.dao.NoticeRuleDao;
|
||||
import org.apache.hertzbeat.alert.dao.NoticeTemplateDao;
|
||||
import org.apache.hertzbeat.alert.notice.AlertNoticeDispatch;
|
||||
import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl;
|
||||
import org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil;
|
||||
import org.apache.hertzbeat.common.cache.CacheFactory;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
@@ -48,12 +49,15 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -211,11 +215,70 @@ class NoticeConfigServiceTest {
|
||||
|
||||
@Test
|
||||
void editReceiver() {
|
||||
final NoticeReceiver noticeReceiver = mock(NoticeReceiver.class);
|
||||
final NoticeReceiver noticeReceiver = new NoticeReceiver();
|
||||
noticeReceiver.setId(5L);
|
||||
when(noticeReceiverDao.findById(5L)).thenReturn(Optional.of(noticeReceiver));
|
||||
noticeConfigService.editReceiver(noticeReceiver);
|
||||
verify(noticeReceiverDao, times(1)).save(noticeReceiver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void editReceiverKeepsStoredSecretWhenMasked() {
|
||||
final NoticeReceiver stored = new NoticeReceiver();
|
||||
stored.setId(5L);
|
||||
stored.setTgBotToken("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM");
|
||||
when(noticeReceiverDao.findById(5L)).thenReturn(Optional.of(stored));
|
||||
|
||||
final NoticeReceiver incoming = new NoticeReceiver();
|
||||
incoming.setId(5L);
|
||||
incoming.setTgBotToken(NoticeReceiverMaskUtil.SECRET_MASK + "voJM");
|
||||
|
||||
noticeConfigService.editReceiver(incoming);
|
||||
|
||||
assertEquals("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM", incoming.getTgBotToken());
|
||||
verify(noticeReceiverDao, times(1)).save(incoming);
|
||||
}
|
||||
|
||||
@Test
|
||||
void editReceiverRejectsUnknownId() {
|
||||
when(noticeReceiverDao.findById(5L)).thenReturn(Optional.empty());
|
||||
|
||||
final NoticeReceiver incoming = new NoticeReceiver();
|
||||
incoming.setId(5L);
|
||||
incoming.setTgBotToken(NoticeReceiverMaskUtil.SECRET_MASK + "voJM");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> noticeConfigService.editReceiver(incoming));
|
||||
verify(noticeReceiverDao, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendTestMsgResolvesMaskedSecret() {
|
||||
final NoticeReceiver stored = new NoticeReceiver();
|
||||
stored.setId(5L);
|
||||
stored.setTgBotToken("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM");
|
||||
when(noticeReceiverDao.findById(5L)).thenReturn(Optional.of(stored));
|
||||
|
||||
final NoticeReceiver incoming = new NoticeReceiver();
|
||||
incoming.setId(5L);
|
||||
incoming.setTgBotToken(NoticeReceiverMaskUtil.SECRET_MASK + "voJM");
|
||||
|
||||
noticeConfigService.sendTestMsg(incoming);
|
||||
|
||||
assertEquals("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM", incoming.getTgBotToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendTestMsgRejectsUnknownId() {
|
||||
when(noticeReceiverDao.findById(5L)).thenReturn(Optional.empty());
|
||||
|
||||
final NoticeReceiver incoming = new NoticeReceiver();
|
||||
incoming.setId(5L);
|
||||
incoming.setTgBotToken(NoticeReceiverMaskUtil.SECRET_MASK + "voJM");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> noticeConfigService.sendTestMsg(incoming));
|
||||
verify(dispatcherAlarm, never()).sendNoticeMsg(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteReceiver() {
|
||||
final Long receiverId = 23342525L;
|
||||
@@ -288,7 +351,7 @@ class NoticeConfigServiceTest {
|
||||
|
||||
@Test
|
||||
void sendTestMsg() {
|
||||
final NoticeReceiver noticeReceiver = mock(NoticeReceiver.class);
|
||||
final NoticeReceiver noticeReceiver = new NoticeReceiver();
|
||||
final NoticeTemplate noticeTemplate = null;
|
||||
noticeConfigService.sendTestMsg(noticeReceiver);
|
||||
verify(dispatcherAlarm, times(1)).sendNoticeMsg(eq(noticeReceiver), eq(noticeTemplate), any(GroupAlert.class));
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.alert.util;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.apache.hertzbeat.alert.util.NoticeReceiverMaskUtil.SECRET_MASK;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* Test case for {@link NoticeReceiverMaskUtil}
|
||||
*/
|
||||
class NoticeReceiverMaskUtilTest {
|
||||
|
||||
private NoticeReceiver buildReceiverWithSecrets() {
|
||||
NoticeReceiver receiver = new NoticeReceiver();
|
||||
receiver.setId(1L);
|
||||
receiver.setName("tom");
|
||||
receiver.setEmail("tom@usthe.com");
|
||||
receiver.setHookUrl("https://example.com/hook");
|
||||
receiver.setHookAuthToken("hook-auth-token-abcd");
|
||||
receiver.setAccessToken("c03a568a306f8fd84dab51ff03cf6af6ba676a3be940c904e1df2de34853739d");
|
||||
receiver.setTgBotToken("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM");
|
||||
receiver.setSlackWebHookUrl("https://hooks.slack.com/services/X/Y/Zt0k3n");
|
||||
receiver.setAppSecret("oUydwn92ey0lnuY02MixNa57eNK-20dJn5NEOG-u2uE");
|
||||
receiver.setDiscordBotToken("MTA2NTMwMzU0ODY4Mzg4MjUzNw.discord.t0kn");
|
||||
receiver.setSmnAk("NCVBODJOEYHSW3VNSMAK");
|
||||
receiver.setSmnSk("nmSNhUJN9MlpPl8lfCsgdA0KvHCL9JSMSK");
|
||||
receiver.setServerChanToken("SCT193569TSNm6xIabdjqeZPtOGOWcvU1e");
|
||||
receiver.setGotifyToken("A845h__ZMqDxZlO");
|
||||
receiver.setNtfyToken("tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2");
|
||||
return receiver;
|
||||
}
|
||||
|
||||
@Test
|
||||
void maskKeepsSuffixOfLongSecrets() {
|
||||
NoticeReceiver receiver = buildReceiverWithSecrets();
|
||||
NoticeReceiver masked = NoticeReceiverMaskUtil.mask(receiver);
|
||||
|
||||
assertEquals(SECRET_MASK + "abcd", masked.getHookAuthToken());
|
||||
assertEquals(SECRET_MASK + "739d", masked.getAccessToken());
|
||||
assertEquals(SECRET_MASK + "voJM", masked.getTgBotToken());
|
||||
assertEquals(SECRET_MASK + "0k3n", masked.getSlackWebHookUrl());
|
||||
assertEquals(SECRET_MASK + "u2uE", masked.getAppSecret());
|
||||
assertEquals(SECRET_MASK + "t0kn", masked.getDiscordBotToken());
|
||||
assertEquals(SECRET_MASK + "SMAK", masked.getSmnAk());
|
||||
assertEquals(SECRET_MASK + "SMSK", masked.getSmnSk());
|
||||
assertEquals(SECRET_MASK + "vU1e", masked.getServerChanToken());
|
||||
assertEquals(SECRET_MASK + "xZlO", masked.getGotifyToken());
|
||||
assertEquals(SECRET_MASK + "NIz2", masked.getNtfyToken());
|
||||
|
||||
assertEquals(receiver.getId(), masked.getId());
|
||||
assertEquals(receiver.getName(), masked.getName());
|
||||
assertEquals(receiver.getEmail(), masked.getEmail());
|
||||
assertEquals(receiver.getHookUrl(), masked.getHookUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void maskHidesShortSecretsEntirely() {
|
||||
NoticeReceiver receiver = new NoticeReceiver();
|
||||
receiver.setAccessToken("short-token");
|
||||
receiver.setGotifyToken("tiny");
|
||||
NoticeReceiver masked = NoticeReceiverMaskUtil.mask(receiver);
|
||||
|
||||
assertEquals(SECRET_MASK, masked.getAccessToken());
|
||||
assertEquals(SECRET_MASK, masked.getGotifyToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
void maskDoesNotModifyOriginalAndKeepsEmptySecrets() {
|
||||
NoticeReceiver receiver = buildReceiverWithSecrets();
|
||||
NoticeReceiverMaskUtil.mask(receiver);
|
||||
|
||||
assertEquals("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM", receiver.getTgBotToken());
|
||||
assertEquals("nmSNhUJN9MlpPl8lfCsgdA0KvHCL9JSMSK", receiver.getSmnSk());
|
||||
|
||||
NoticeReceiver empty = new NoticeReceiver();
|
||||
NoticeReceiver maskedEmpty = NoticeReceiverMaskUtil.mask(empty);
|
||||
assertNull(maskedEmpty.getAccessToken());
|
||||
assertNull(maskedEmpty.getNtfyToken());
|
||||
assertNull(NoticeReceiverMaskUtil.mask(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMaskRestoresOnlyMaskedFields() {
|
||||
NoticeReceiver existing = buildReceiverWithSecrets();
|
||||
NoticeReceiver incoming = NoticeReceiverMaskUtil.mask(existing);
|
||||
incoming.setAccessToken("new-access-token-1234");
|
||||
incoming.setGotifyToken(null);
|
||||
incoming.setNtfyToken(SECRET_MASK);
|
||||
|
||||
NoticeReceiverMaskUtil.resolveMask(incoming, existing);
|
||||
|
||||
assertEquals("new-access-token-1234", incoming.getAccessToken());
|
||||
assertNull(incoming.getGotifyToken());
|
||||
assertEquals("tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", incoming.getNtfyToken());
|
||||
assertEquals("hook-auth-token-abcd", incoming.getHookAuthToken());
|
||||
assertEquals("1499012345:AAEOB_wEYS-DZyPM3h5NzI8voJM", incoming.getTgBotToken());
|
||||
assertEquals("https://hooks.slack.com/services/X/Y/Zt0k3n", incoming.getSlackWebHookUrl());
|
||||
assertEquals("oUydwn92ey0lnuY02MixNa57eNK-20dJn5NEOG-u2uE", incoming.getAppSecret());
|
||||
assertEquals("MTA2NTMwMzU0ODY4Mzg4MjUzNw.discord.t0kn", incoming.getDiscordBotToken());
|
||||
assertEquals("NCVBODJOEYHSW3VNSMAK", incoming.getSmnAk());
|
||||
assertEquals("nmSNhUJN9MlpPl8lfCsgdA0KvHCL9JSMSK", incoming.getSmnSk());
|
||||
assertEquals("SCT193569TSNm6xIabdjqeZPtOGOWcvU1e", incoming.getServerChanToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMaskIgnoresMaskWhenNothingIsStored() {
|
||||
NoticeReceiver existing = new NoticeReceiver();
|
||||
NoticeReceiver incoming = new NoticeReceiver();
|
||||
incoming.setAccessToken(SECRET_MASK);
|
||||
|
||||
NoticeReceiverMaskUtil.resolveMask(incoming, existing);
|
||||
|
||||
assertEquals(SECRET_MASK, incoming.getAccessToken());
|
||||
}
|
||||
}
|
||||
+2
@@ -222,6 +222,8 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
String monitorKey;
|
||||
if (metrics.isHasSubTask()) {
|
||||
monitorKey = job.getId() + "-" + metrics.getName() + "-sub-" + metrics.getSubTaskId();
|
||||
} else if (metrics.getPrometheus() != null) {
|
||||
monitorKey = String.valueOf(job.getId());
|
||||
} else {
|
||||
monitorKey = job.getId() + "-" + metrics.getName();
|
||||
}
|
||||
|
||||
-19
@@ -1,22 +1,3 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.common.entity.dto.observability;
|
||||
|
||||
/**
|
||||
* Which observability query consoles the current storage backend supports.
|
||||
*
|
||||
* @param log log query console supported
|
||||
* @param trace trace query console supported
|
||||
* @param metric OTLP metric query console supported
|
||||
*/
|
||||
public record ObservabilityCapability(boolean log, boolean trace, boolean metric) {
|
||||
}
|
||||
-19
@@ -1,22 +1,3 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
|
||||
-19
@@ -1,22 +1,3 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
|
||||
package org.apache.hertzbeat.common.util;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
@@ -46,6 +49,12 @@ public final class AesUtil {
|
||||
|
||||
private static final String AES = "AES";
|
||||
|
||||
private static final byte[] PAYLOAD_HEADER = {'H', 'B', 'A', '2'};
|
||||
|
||||
private static final int IV_LENGTH = 16;
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
/**
|
||||
* Encryption key The AES encryption key is 16 bits.
|
||||
* If the AES encryption key is larger than 16 bits, an error message is displayed
|
||||
@@ -87,14 +96,21 @@ public final class AesUtil {
|
||||
SecretKeySpec keySpec = new SecretKeySpec(encryptKey.getBytes(StandardCharsets.UTF_8), AES);
|
||||
// cipher based on the algorithm AES
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM_STR);
|
||||
byte[] initializationVector = new byte[IV_LENGTH];
|
||||
SECURE_RANDOM.nextBytes(initializationVector);
|
||||
// init cipher Encrypt_mode or Decrypt_mode operation, the second parameter is the KEY used
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(encryptKey.getBytes(StandardCharsets.UTF_8)));
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(initializationVector));
|
||||
// get content bytes, must utf-8
|
||||
byte[] byteEncode = content.getBytes(StandardCharsets.UTF_8);
|
||||
// encode content to byte array
|
||||
byte[] byteAes = cipher.doFinal(byteEncode);
|
||||
byte[] payload = ByteBuffer.allocate(PAYLOAD_HEADER.length + initializationVector.length + byteAes.length)
|
||||
.put(PAYLOAD_HEADER)
|
||||
.put(initializationVector)
|
||||
.put(byteAes)
|
||||
.array();
|
||||
// base64 encode content
|
||||
return Base64.getEncoder().encodeToString(byteAes);
|
||||
return Base64.getEncoder().encodeToString(payload);
|
||||
} catch (Exception e) {
|
||||
log.error("aes encode content error: {}", e.getMessage(), e);
|
||||
return content;
|
||||
@@ -135,13 +151,36 @@ public final class AesUtil {
|
||||
SecretKeySpec keySpec = new SecretKeySpec(decryptKey.getBytes(StandardCharsets.UTF_8), AES);
|
||||
// cipher based on the algorithm AES
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM_STR);
|
||||
// init cipher Encrypt_mode or Decrypt_mode operation, the second parameter is the KEY used
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(decryptKey.getBytes(StandardCharsets.UTF_8)));
|
||||
// base64 decode content
|
||||
byte[] bytesContent = Base64.getDecoder().decode(content);
|
||||
byte[] initializationVector;
|
||||
if (hasPayloadHeader(bytesContent)) {
|
||||
if (bytesContent.length <= PAYLOAD_HEADER.length + IV_LENGTH) {
|
||||
throw new IllegalArgumentException("Invalid encrypted payload");
|
||||
}
|
||||
initializationVector = Arrays.copyOfRange(
|
||||
bytesContent, PAYLOAD_HEADER.length, PAYLOAD_HEADER.length + IV_LENGTH);
|
||||
bytesContent = Arrays.copyOfRange(
|
||||
bytesContent, PAYLOAD_HEADER.length + IV_LENGTH, bytesContent.length);
|
||||
} else {
|
||||
initializationVector = decryptKey.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
// init cipher Encrypt_mode or Decrypt_mode operation, the second parameter is the KEY used
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(initializationVector));
|
||||
// decode content to byte array
|
||||
return cipher.doFinal(bytesContent);
|
||||
}
|
||||
|
||||
private static boolean hasPayloadHeader(byte[] payload) {
|
||||
if (payload.length < PAYLOAD_HEADER.length) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < PAYLOAD_HEADER.length; index++) {
|
||||
if (payload[index] != PAYLOAD_HEADER[index]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether it is encrypted
|
||||
|
||||
@@ -20,10 +20,14 @@ package org.apache.hertzbeat.common.util;
|
||||
import static org.apache.hertzbeat.common.util.AesUtil.aesDecode;
|
||||
import static org.apache.hertzbeat.common.util.AesUtil.aesEncode;
|
||||
import static org.apache.hertzbeat.common.util.AesUtil.isCiphertext;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
@@ -43,12 +47,31 @@ class AesUtilTest {
|
||||
void testAesEncode() {
|
||||
String originalText = "This is a secret message";
|
||||
String encryptedText = aesEncode(originalText, VALID_KEY);
|
||||
String secondEncryptedText = aesEncode(originalText, VALID_KEY);
|
||||
assertNotEquals(originalText, encryptedText);
|
||||
assertNotEquals(encryptedText, secondEncryptedText);
|
||||
|
||||
byte[] encryptedPayload = Base64.getDecoder().decode(encryptedText);
|
||||
byte[] secondEncryptedPayload = Base64.getDecoder().decode(secondEncryptedText);
|
||||
assertArrayEquals(new byte[] {'H', 'B', 'A', '2'}, Arrays.copyOfRange(encryptedPayload, 0, 4));
|
||||
assertFalse(Arrays.equals(
|
||||
VALID_KEY.getBytes(StandardCharsets.UTF_8), Arrays.copyOfRange(encryptedPayload, 4, 20)));
|
||||
assertFalse(Arrays.equals(
|
||||
Arrays.copyOfRange(encryptedPayload, 4, 20),
|
||||
Arrays.copyOfRange(secondEncryptedPayload, 4, 20)));
|
||||
|
||||
String decryptedText = aesDecode(encryptedText, VALID_KEY);
|
||||
assertEquals(originalText, decryptedText);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLegacyCiphertextCanBeDecoded() {
|
||||
String legacyCiphertext = "muJNZwhxg173v6EZAXb5TK8L5XlmDE5xmc9XTryH6Qk=";
|
||||
|
||||
assertEquals("This is a secret message", aesDecode(legacyCiphertext, VALID_KEY));
|
||||
assertTrue(isCiphertext(legacyCiphertext, VALID_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAesDecode() {
|
||||
// Test with invalid key
|
||||
|
||||
-19
@@ -17,25 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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.common.entity.alerter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
@Entity
|
||||
@Table(name = "hzb_notice_receiver")
|
||||
@Data
|
||||
@Builder
|
||||
@Builder(toBuilder = true)
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "Message notification recipient entity")
|
||||
|
||||
@@ -70,6 +70,7 @@ resourceRole:
|
||||
- /api/sse/**===post===[admin,user]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin,user]
|
||||
- /api/logs/sse/**===get===[admin,user,guest]
|
||||
- /api/logs/ingest/**===post===[admin,user]
|
||||
- /api/otlp/**===post===[admin,user]
|
||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||
@@ -81,10 +82,10 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alert/sse/**===*
|
||||
- /api/logs/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
- /api/observability/capability===get
|
||||
- /api/push/**===*
|
||||
- /api/status/page/public/**===*
|
||||
- /api/manager/sse/**===*
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
|
||||
<properties>
|
||||
<awaitility.version>4.2.0</awaitility.version>
|
||||
<grpc.version>1.56.1</grpc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -72,21 +71,18 @@
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java-util</artifactId>
|
||||
</dependency>
|
||||
<!-- OTLP gRPC server -->
|
||||
<!-- OTLP gRPC server, version managed by grpc-bom in the root pom -->
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<!-- awaitility for async testing -->
|
||||
<dependency>
|
||||
|
||||
+4
@@ -44,6 +44,7 @@ import org.springframework.web.client.RestTemplate;
|
||||
public class GreptimeSignalInitializer {
|
||||
|
||||
private static final String TRACE_SCHEMA = "greptime/tables/hzb_traces.sql";
|
||||
private static final String LOG_SCHEMA = "greptime/tables/hertzbeat_logs.sql";
|
||||
private static final String LOG_PIPELINE = "greptime/pipelines/hertzbeat_otlp_log_v1.yaml";
|
||||
private final GreptimeProperties greptimeProperties;
|
||||
private final GreptimeSqlQueryExecutor sqlQueryExecutor;
|
||||
@@ -68,6 +69,9 @@ public class GreptimeSignalInitializer {
|
||||
+ "\"resource_attributes.service.namespace\" STRING NULL");
|
||||
sqlQueryExecutor.execute("ALTER TABLE hzb_traces ADD COLUMN IF NOT EXISTS "
|
||||
+ "\"resource_attributes.deployment.environment.name\" STRING NULL");
|
||||
String logSchema = new ClassPathResource(LOG_SCHEMA)
|
||||
.getContentAsString(StandardCharsets.UTF_8).strip();
|
||||
sqlQueryExecutor.execute(StringUtils.trimTrailingCharacter(logSchema, ';'));
|
||||
uploadLogPipeline(new ClassPathResource(LOG_PIPELINE)
|
||||
.getContentAsString(StandardCharsets.UTF_8));
|
||||
if (sqlQueryExecutor.execute("SELECT 1 AS ready").isEmpty()) {
|
||||
|
||||
+17
-4
@@ -24,6 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -32,6 +33,7 @@ import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.apache.hertzbeat.common.entity.dto.observability.LogQueryFilter;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
@@ -54,6 +56,9 @@ public class LogQueryController {
|
||||
|
||||
private static final int MAX_PAGE_SIZE = 200;
|
||||
|
||||
private static final String LOG_STORAGE_UNAVAILABLE_MSG =
|
||||
"Log query is not supported by the current storage, please enable a log capable storage such as GreptimeDB";
|
||||
|
||||
private final HistoryDataReader historyDataReader;
|
||||
private final SignalWorkloadGuard workloadGuard;
|
||||
|
||||
@@ -88,7 +93,7 @@ public class LogQueryController {
|
||||
@RequestParam(value = "pageIndex", required = false, defaultValue = "0") Integer pageIndex,
|
||||
@Parameter(description = "Number of items per page", example = "20")
|
||||
@RequestParam(value = "pageSize", required = false, defaultValue = "20") Integer pageSize) {
|
||||
return workloadGuard.execute(Workload.LOG_LIST, () -> {
|
||||
return executeIfLogSupported(Workload.LOG_LIST, () -> {
|
||||
LogQueryFilter filter = filter(start, end, traceId, spanId, severityNumber, severityText, search,
|
||||
serviceName, serviceNamespace, environment, resource);
|
||||
Page<LogEntry> result = getPagedLogs(filter, pageIndex, pageSize);
|
||||
@@ -118,7 +123,7 @@ public class LogQueryController {
|
||||
@RequestParam(value = "serviceNamespace", required = false) String serviceNamespace,
|
||||
@RequestParam(value = "environment", required = false) String environment,
|
||||
@RequestParam(value = "resource", required = false) String resource) {
|
||||
return workloadGuard.execute(Workload.LOG_AGGREGATE,
|
||||
return executeIfLogSupported(Workload.LOG_AGGREGATE,
|
||||
() -> doOverviewStats(filter(start, end, traceId, spanId, severityNumber, severityText, search,
|
||||
serviceName, serviceNamespace, environment, resource)));
|
||||
}
|
||||
@@ -149,7 +154,7 @@ public class LogQueryController {
|
||||
@RequestParam(value = "serviceNamespace", required = false) String serviceNamespace,
|
||||
@RequestParam(value = "environment", required = false) String environment,
|
||||
@RequestParam(value = "resource", required = false) String resource) {
|
||||
return workloadGuard.execute(Workload.LOG_AGGREGATE,
|
||||
return executeIfLogSupported(Workload.LOG_AGGREGATE,
|
||||
() -> doTraceCoverageStats(filter(start, end, traceId, spanId, severityNumber, severityText, search,
|
||||
serviceName, serviceNamespace, environment, resource)));
|
||||
}
|
||||
@@ -182,7 +187,7 @@ public class LogQueryController {
|
||||
@RequestParam(value = "serviceNamespace", required = false) String serviceNamespace,
|
||||
@RequestParam(value = "environment", required = false) String environment,
|
||||
@RequestParam(value = "resource", required = false) String resource) {
|
||||
return workloadGuard.execute(Workload.LOG_AGGREGATE,
|
||||
return executeIfLogSupported(Workload.LOG_AGGREGATE,
|
||||
() -> doTrendStats(filter(start, end, traceId, spanId, severityNumber, severityText, search,
|
||||
serviceName, serviceNamespace, environment, resource)));
|
||||
}
|
||||
@@ -193,6 +198,14 @@ public class LogQueryController {
|
||||
return ResponseEntity.ok(Message.success(result));
|
||||
}
|
||||
|
||||
private <T> ResponseEntity<Message<T>> executeIfLogSupported(Workload workload,
|
||||
Supplier<ResponseEntity<Message<T>>> operation) {
|
||||
if (!historyDataReader.supportsLogQuery()) {
|
||||
return ResponseEntity.ok(Message.fail(CommonConstants.FAIL_CODE, LOG_STORAGE_UNAVAILABLE_MSG));
|
||||
}
|
||||
return workloadGuard.execute(workload, operation);
|
||||
}
|
||||
|
||||
private LogQueryFilter filter(Long start, Long end, String traceId, String spanId, Integer severityNumber,
|
||||
String severityText, String search, String serviceName, String serviceNamespace,
|
||||
String environment, String resource) {
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.log.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.apache.hertzbeat.common.entity.dto.observability.ObservabilityCapability;
|
||||
import org.apache.hertzbeat.log.service.ThreeSignalQueryService;
|
||||
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Report which observability query features the current storage backend supports,
|
||||
* so the UI can render guidance instead of firing queries that cannot be served.
|
||||
*
|
||||
* <p>The three capabilities are probed through two different mechanisms on purpose: log query
|
||||
* is dispatched per storage via {@link HistoryDataReader#supportsLogQuery()} (multiple storages
|
||||
* implement log read/write), while trace/metric consoles are served only by the conditionally
|
||||
* registered {@link ThreeSignalQueryService} (GreptimeDB only for now).
|
||||
* TODO: once trace/metric query grows storage-dispatched implementations like logs (or log query
|
||||
* is absorbed into the three-signal contract), unify the probes on a single abstraction.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/observability", produces = "application/json")
|
||||
@Tag(name = "Observability Capability Controller")
|
||||
public class ObservabilityCapabilityController {
|
||||
|
||||
private final HistoryDataReader historyDataReader;
|
||||
private final ObjectProvider<ThreeSignalQueryService> threeSignalQueryService;
|
||||
|
||||
public ObservabilityCapabilityController(HistoryDataReader historyDataReader,
|
||||
ObjectProvider<ThreeSignalQueryService> threeSignalQueryService) {
|
||||
this.historyDataReader = historyDataReader;
|
||||
this.threeSignalQueryService = threeSignalQueryService;
|
||||
}
|
||||
|
||||
@GetMapping("/capability")
|
||||
@Operation(summary = "Query observability capability",
|
||||
description = "Return whether log, trace and OTLP metric query consoles are supported by the current storage backend.")
|
||||
public ResponseEntity<Message<ObservabilityCapability>> capability() {
|
||||
boolean traceAndMetricSupported = threeSignalQueryService.getIfAvailable() != null;
|
||||
return ResponseEntity.ok(Message.success(new ObservabilityCapability(
|
||||
historyDataReader.supportsLogQuery(), traceAndMetricSupported, traceAndMetricSupported)));
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
@@ -51,6 +52,8 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
public class LogSseManager {
|
||||
|
||||
private static final long BATCH_INTERVAL_MS = 200;
|
||||
private static final long HEARTBEAT_INTERVAL_SECONDS = 15;
|
||||
private static final long RECONNECT_TIME_MS = 5_000;
|
||||
private static final int MAX_BATCH_SIZE = 1000;
|
||||
private static final int MAX_QUEUE_SIZE = 10000;
|
||||
|
||||
@@ -70,6 +73,8 @@ public class LogSseManager {
|
||||
|
||||
public LogSseManager() {
|
||||
scheduler.scheduleAtFixedRate(this::flushBatch, BATCH_INTERVAL_MS, BATCH_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
scheduler.scheduleAtFixedRate(this::sendHeartbeat, HEARTBEAT_INTERVAL_SECONDS,
|
||||
HEARTBEAT_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@@ -96,6 +101,13 @@ public class LogSseManager {
|
||||
emitter.onError((ex) -> removeEmitter(clientId));
|
||||
|
||||
emitters.put(clientId, new SseSubscriber(emitter, filters));
|
||||
try {
|
||||
emitter.send(SseEmitter.event()
|
||||
.comment("connected")
|
||||
.reconnectTime(RECONNECT_TIME_MS));
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
safeComplete(clientId, emitter);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@@ -167,6 +179,31 @@ public class LogSseManager {
|
||||
}
|
||||
}
|
||||
|
||||
void sendHeartbeat() {
|
||||
for (Map.Entry<Long, SseSubscriber> entry : emitters.entrySet()) {
|
||||
Long clientId = entry.getKey();
|
||||
SseEmitter emitter = entry.getValue().emitter;
|
||||
try {
|
||||
senderPool.submit(() -> sendHeartbeat(clientId, emitter));
|
||||
} catch (RejectedExecutionException e) {
|
||||
if (!senderPool.isShutdown()) {
|
||||
log.warn("Failed to schedule SSE heartbeat for client {}", clientId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendHeartbeat(Long clientId, SseEmitter emitter) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().comment("heartbeat"));
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
safeComplete(clientId, emitter);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send SSE heartbeat to client {}", clientId, e);
|
||||
safeComplete(clientId, emitter);
|
||||
}
|
||||
}
|
||||
|
||||
private void safeComplete(Long clientId, SseEmitter emitter) {
|
||||
try {
|
||||
emitter.complete();
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
-- 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.
|
||||
|
||||
-- Schema must stay in sync with the transform section of
|
||||
-- greptime/pipelines/hertzbeat_otlp_log_v1.yaml, which GreptimeDB would
|
||||
-- otherwise use to auto-create this table on first ingestion.
|
||||
CREATE TABLE IF NOT EXISTS hertzbeat_logs (
|
||||
"time_unix_nano" TIMESTAMP(9) TIME INDEX,
|
||||
"observed_time_unix_nano" TIMESTAMP(9) NULL,
|
||||
"trace_id" STRING NULL SKIPPING INDEX WITH(granularity = '10240', type = 'BLOOM'),
|
||||
"span_id" STRING NULL SKIPPING INDEX WITH(granularity = '10240', type = 'BLOOM'),
|
||||
"trace_flags" INT NULL,
|
||||
"severity_text" STRING NULL,
|
||||
"severity_number" INT NULL,
|
||||
"body" STRING NULL,
|
||||
"attributes" JSON NULL,
|
||||
"resource" JSON NULL,
|
||||
"instrumentation_scope" JSON NULL,
|
||||
"dropped_attributes_count" INT NULL
|
||||
) WITH ('append_mode' = 'true');
|
||||
@@ -35,4 +35,4 @@ CREATE TABLE IF NOT EXISTS hzb_traces (
|
||||
"span_events" JSON NULL,
|
||||
"span_links" JSON NULL,
|
||||
PRIMARY KEY("service_name")
|
||||
) WITH (append_mode = true, table_data_model = 'greptime_trace_v1');
|
||||
) WITH ('append_mode' = 'true', 'table_data_model' = 'greptime_trace_v1');
|
||||
|
||||
+2
@@ -76,6 +76,8 @@ class GreptimeSignalInitializerTest {
|
||||
"ALTER TABLE hzb_traces ADD COLUMN IF NOT EXISTS \"resource_attributes.service.namespace\""));
|
||||
sqlOrder.verify(sqlQueryExecutor).execute(org.mockito.ArgumentMatchers.startsWith(
|
||||
"ALTER TABLE hzb_traces ADD COLUMN IF NOT EXISTS \"resource_attributes.deployment.environment.name\""));
|
||||
sqlOrder.verify(sqlQueryExecutor).execute(org.mockito.ArgumentMatchers.contains(
|
||||
"CREATE TABLE IF NOT EXISTS hertzbeat_logs"));
|
||||
sqlOrder.verify(sqlQueryExecutor).execute("SELECT 1 AS ready");
|
||||
verify(restTemplate).exchange(eq("http://127.0.0.1:4000/v1/pipelines/hertzbeat_otlp_log_v1"),
|
||||
eq(HttpMethod.POST), any(HttpEntity.class), eq(String.class));
|
||||
|
||||
+15
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.log.controller;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -57,12 +58,26 @@ class LogQueryControllerTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(historyDataReader.supportsLogQuery()).thenReturn(true);
|
||||
this.logQueryController = new LogQueryController(historyDataReader, new SignalWorkloadGuard());
|
||||
this.mockMvc = MockMvcBuilders.standaloneSetup(logQueryController)
|
||||
.setControllerAdvice(new SignalWorkloadExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFriendlyFailureWhenLogQueryUnsupported() throws Exception {
|
||||
when(historyDataReader.supportsLogQuery()).thenReturn(false);
|
||||
|
||||
for (String path : List.of("/api/logs/list", "/api/logs/stats/overview",
|
||||
"/api/logs/stats/trace-coverage", "/api/logs/stats/trend")) {
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(path))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
|
||||
.andExpect(jsonPath("$.msg").isNotEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListLogsWithAllFilters() throws Exception {
|
||||
// Mock data
|
||||
|
||||
+26
-1
@@ -17,9 +17,11 @@
|
||||
|
||||
package org.apache.hertzbeat.log.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.apache.hertzbeat.log.notice.LogSseFilterCriteria;
|
||||
@@ -35,6 +37,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
/**
|
||||
@@ -75,6 +78,28 @@ class LogSseControllerTest {
|
||||
Assertions.assertNull(capturedCriteria.getSpanId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscribeCommitsInitialEvent() throws Exception {
|
||||
LogSseManager realEmitterManager = new LogSseManager();
|
||||
try {
|
||||
MockMvc realMockMvc = MockMvcBuilders
|
||||
.standaloneSetup(new LogSseController(realEmitterManager))
|
||||
.build();
|
||||
|
||||
MvcResult result = realMockMvc.perform(get("/api/logs/sse/subscribe")
|
||||
.accept(MediaType.TEXT_EVENT_STREAM_VALUE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(request().asyncStarted())
|
||||
.andReturn();
|
||||
|
||||
assertTrue(result.getResponse().isCommitted());
|
||||
assertTrue(result.getResponse().getContentAsString().contains(":connected"));
|
||||
assertTrue(result.getResponse().getContentAsString().contains("retry:5000"));
|
||||
} finally {
|
||||
realEmitterManager.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscribeWithMultipleFilters() throws Exception {
|
||||
// Given: Multiple filter parameters
|
||||
@@ -110,4 +135,4 @@ class LogSseControllerTest {
|
||||
.accept(MediaType.TEXT_EVENT_STREAM_VALUE))
|
||||
.andExpect(status().is4xxClientError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.log.controller;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.log.service.ThreeSignalQueryService;
|
||||
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
/**
|
||||
* Unit test for {@link ObservabilityCapabilityController}
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ObservabilityCapabilityControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Mock
|
||||
private HistoryDataReader historyDataReader;
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<ThreeSignalQueryService> threeSignalQueryService;
|
||||
|
||||
@Mock
|
||||
private ThreeSignalQueryService queryService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.mockMvc = MockMvcBuilders.standaloneSetup(
|
||||
new ObservabilityCapabilityController(historyDataReader, threeSignalQueryService)).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportAllUnsupportedWithoutCapableStorage() throws Exception {
|
||||
when(historyDataReader.supportsLogQuery()).thenReturn(false);
|
||||
when(threeSignalQueryService.getIfAvailable()).thenReturn(null);
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/capability"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.data.log").value(false))
|
||||
.andExpect(jsonPath("$.data.trace").value(false))
|
||||
.andExpect(jsonPath("$.data.metric").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportAllSupportedWithCapableStorage() throws Exception {
|
||||
when(historyDataReader.supportsLogQuery()).thenReturn(true);
|
||||
when(threeSignalQueryService.getIfAvailable()).thenReturn(queryService);
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/capability"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.data.log").value(true))
|
||||
.andExpect(jsonPath("$.data.trace").value(true))
|
||||
.andExpect(jsonPath("$.data.metric").value(true));
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,33 @@ class LogSseManagerTest {
|
||||
assertTrue(logSseManager.getEmitters().containsKey(CLIENT_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSendHeartbeatToSubscribers() throws IOException {
|
||||
SseEmitter mockEmitter = mock(SseEmitter.class);
|
||||
subscribeClient(CLIENT_ID, null, mockEmitter);
|
||||
|
||||
logSseManager.sendHeartbeat();
|
||||
|
||||
await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() ->
|
||||
verify(mockEmitter).send(any(SseEmitter.SseEventBuilder.class))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveEmitterWhenHeartbeatFails() throws IOException {
|
||||
SseEmitter mockEmitter = mock(SseEmitter.class);
|
||||
doThrow(new IOException("Connection closed"))
|
||||
.when(mockEmitter).send(any(SseEmitter.SseEventBuilder.class));
|
||||
subscribeClient(CLIENT_ID, null, mockEmitter);
|
||||
|
||||
logSseManager.sendHeartbeat();
|
||||
|
||||
await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() -> {
|
||||
verify(mockEmitter).complete();
|
||||
assertFalse(logSseManager.getEmitters().containsKey(CLIENT_ID));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBroadcastLogWhenFilterMatches() throws IOException {
|
||||
// Given: A client with a filter for "INFO" logs
|
||||
|
||||
+9
-4
@@ -62,15 +62,20 @@ public class StatusPagePublicController {
|
||||
|
||||
@GetMapping("/component")
|
||||
@Operation(summary = "Query Status Page Components")
|
||||
public ResponseEntity<Message<List<ComponentStatus>>> queryStatusPageComponent() {
|
||||
List<ComponentStatus> componentStatusList = statusPageService.queryComponentsStatus();
|
||||
public ResponseEntity<Message<List<ComponentStatus>>> queryStatusPageComponent(
|
||||
@Parameter(description = "Number of days of history", example = "30")
|
||||
@RequestParam(defaultValue = "30") int days) {
|
||||
List<ComponentStatus> componentStatusList = statusPageService.queryComponentsStatus(days);
|
||||
return ResponseEntity.ok(Message.success(componentStatusList));
|
||||
}
|
||||
|
||||
@GetMapping("/component/{id}")
|
||||
@Operation(summary = "Query Status Page Component")
|
||||
public ResponseEntity<Message<ComponentStatus>> queryStatusPageComponent(@PathVariable("id") final long id) {
|
||||
ComponentStatus componentStatus = statusPageService.queryComponentStatus(id);
|
||||
public ResponseEntity<Message<ComponentStatus>> queryStatusPageComponent(
|
||||
@PathVariable("id") final long id,
|
||||
@Parameter(description = "Number of days of history", example = "30")
|
||||
@RequestParam(defaultValue = "30") int days) {
|
||||
ComponentStatus componentStatus = statusPageService.queryComponentStatus(id, days);
|
||||
return ResponseEntity.ok(Message.success(componentStatus));
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -83,17 +83,19 @@ public interface StatusPageService {
|
||||
/**
|
||||
* query status page components status.
|
||||
*
|
||||
* @param days number of days of history to return
|
||||
* @return status page components status
|
||||
*/
|
||||
List<ComponentStatus> queryComponentsStatus();
|
||||
List<ComponentStatus> queryComponentsStatus(int days);
|
||||
|
||||
/**
|
||||
* query status page component status.
|
||||
*
|
||||
* @param id status page component id
|
||||
* @param id status page component id
|
||||
* @param days number of days of history to return
|
||||
* @return status page component status
|
||||
*/
|
||||
ComponentStatus queryComponentStatus(long id);
|
||||
ComponentStatus queryComponentStatus(long id, int days);
|
||||
|
||||
/**
|
||||
* query status page incidents.
|
||||
|
||||
+18
-10
@@ -58,8 +58,6 @@ import org.springframework.util.StringUtils;
|
||||
@RequiredArgsConstructor
|
||||
public class StatusPageServiceImpl implements StatusPageService {
|
||||
|
||||
private static final int HISTORY_SPAN_DAYS = 29;
|
||||
|
||||
@Autowired
|
||||
private StatusPageOrgDao statusPageOrgDao;
|
||||
|
||||
@@ -126,7 +124,12 @@ public class StatusPageServiceImpl implements StatusPageService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ComponentStatus> queryComponentsStatus() {
|
||||
public List<ComponentStatus> queryComponentsStatus(int days) {
|
||||
if (days < 1 || days > 90) {
|
||||
throw new IllegalArgumentException("days must be between 1 and 90");
|
||||
}
|
||||
|
||||
int historySpanDays = days - 1;
|
||||
List<StatusPageComponent> components = statusPageComponentDao.findAll();
|
||||
List<ComponentStatus> componentStatusList = new LinkedList<>();
|
||||
for (StatusPageComponent component : components) {
|
||||
@@ -150,11 +153,11 @@ public class StatusPageServiceImpl implements StatusPageService {
|
||||
.findStatusPageHistoriesByComponentIdAndTimestampBetween(component.getId(), todayStartTimestamp, nowTimestamp);
|
||||
StatusPageHistory todayStatus = combineOneDayStatusPageHistory(todayStatusPageHistoryList, component, nowTimestamp);
|
||||
histories.add(todayStatus);
|
||||
// query 30d component status history
|
||||
// query component status history for the configured number of days
|
||||
long preTimestamp = now
|
||||
.atZone(zoneId)
|
||||
.toLocalDate()
|
||||
.minusDays(HISTORY_SPAN_DAYS)
|
||||
.minusDays(historySpanDays)
|
||||
.atStartOfDay(zoneId)
|
||||
.toInstant()
|
||||
.toEpochMilli();
|
||||
@@ -167,7 +170,7 @@ public class StatusPageServiceImpl implements StatusPageService {
|
||||
.atZone(zoneId)
|
||||
.minusSeconds(1); // yesterday 23:59:59 local time
|
||||
|
||||
for (int i = 0; i < HISTORY_SPAN_DAYS; i++) {
|
||||
for (int i = 0; i < historySpanDays; i++) {
|
||||
long endTimestamp = end.toInstant().toEpochMilli();
|
||||
|
||||
long startTimestamp = end.toLocalDate()
|
||||
@@ -242,7 +245,12 @@ public class StatusPageServiceImpl implements StatusPageService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentStatus queryComponentStatus(long id) {
|
||||
public ComponentStatus queryComponentStatus(long id, int days) {
|
||||
if (days < 1 || days > 90) {
|
||||
throw new IllegalArgumentException("days must be between 1 and 90");
|
||||
}
|
||||
|
||||
int historySpanDays = days - 1;
|
||||
StatusPageComponent component = statusPageComponentDao.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("component not found"));
|
||||
|
||||
@@ -272,11 +280,11 @@ public class StatusPageServiceImpl implements StatusPageService {
|
||||
|
||||
histories.add(todayStatus);
|
||||
|
||||
// Previous HISTORY_SPAN_DAYS days (excluding today)
|
||||
// Previous historySpanDays days (excluding today)
|
||||
long preTimestamp = now
|
||||
.atZone(zoneId)
|
||||
.toLocalDate()
|
||||
.minusDays(HISTORY_SPAN_DAYS)
|
||||
.minusDays(historySpanDays)
|
||||
.atStartOfDay(zoneId)
|
||||
.toInstant()
|
||||
.toEpochMilli();
|
||||
@@ -292,7 +300,7 @@ public class StatusPageServiceImpl implements StatusPageService {
|
||||
.atZone(zoneId)
|
||||
.minusSeconds(1); // yesterday 23:59:59 local time
|
||||
|
||||
for (int i = 0; i < HISTORY_SPAN_DAYS; i++) {
|
||||
for (int i = 0; i < historySpanDays; i++) {
|
||||
long endTimestamp = end.toInstant().toEpochMilli();
|
||||
|
||||
long startTimestamp = end.toLocalDate()
|
||||
|
||||
+28
-2
@@ -88,7 +88,7 @@ class StatusPagePublicControllerTest {
|
||||
public void testQueryStatusPageComponent() throws Exception {
|
||||
|
||||
List<ComponentStatus> componentStatusList = Collections.singletonList(new ComponentStatus());
|
||||
when(statusPageService.queryComponentsStatus()).thenReturn(componentStatusList);
|
||||
when(statusPageService.queryComponentsStatus(30)).thenReturn(componentStatusList);
|
||||
|
||||
mockMvc.perform(get("/api/status/page/public/component")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
@@ -96,11 +96,24 @@ class StatusPagePublicControllerTest {
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryStatusPageComponentWithDays() throws Exception {
|
||||
|
||||
List<ComponentStatus> componentStatusList = Collections.singletonList(new ComponentStatus());
|
||||
when(statusPageService.queryComponentsStatus(7)).thenReturn(componentStatusList);
|
||||
|
||||
mockMvc.perform(get("/api/status/page/public/component")
|
||||
.param("days", "7")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryStatusPageComponentById() throws Exception {
|
||||
|
||||
ComponentStatus componentStatus = new ComponentStatus();
|
||||
when(statusPageService.queryComponentStatus(1L)).thenReturn(componentStatus);
|
||||
when(statusPageService.queryComponentStatus(1L, 30)).thenReturn(componentStatus);
|
||||
|
||||
mockMvc.perform(get("/api/status/page/public/component/1")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
@@ -108,6 +121,19 @@ class StatusPagePublicControllerTest {
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryStatusPageComponentByIdWithDays() throws Exception {
|
||||
|
||||
ComponentStatus componentStatus = new ComponentStatus();
|
||||
when(statusPageService.queryComponentStatus(1L, 7)).thenReturn(componentStatus);
|
||||
|
||||
mockMvc.perform(get("/api/status/page/public/component/1")
|
||||
.param("days", "7")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryStatusPageIncident() throws Exception {
|
||||
|
||||
|
||||
+5
-4
@@ -99,8 +99,9 @@ class StatusPageServiceImplTimeTest {
|
||||
when(historyDao.findStatusPageHistoriesByComponentIdAndTimestampBetween(anyLong(), anyLong(), anyLong()))
|
||||
.thenReturn(List.of(before, after));
|
||||
|
||||
List<ComponentStatus> result = service.queryComponentsStatus();
|
||||
assertEquals(30, result.get(0).getHistory().size());
|
||||
assertEquals(30, service.queryComponentsStatus(30).get(0).getHistory().size());
|
||||
assertEquals(1, service.queryComponentsStatus(1).get(0).getHistory().size());
|
||||
assertEquals(90, service.queryComponentsStatus(90).get(0).getHistory().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -114,7 +115,7 @@ class StatusPageServiceImplTimeTest {
|
||||
when(historyDao.findStatusPageHistoriesByComponentIdAndTimestampBetween(anyLong(), anyLong(), anyLong()))
|
||||
.thenReturn(List.of(history));
|
||||
|
||||
List<ComponentStatus> result = service.queryComponentsStatus();
|
||||
List<ComponentStatus> result = service.queryComponentsStatus(30);
|
||||
assertEquals(30, result.get(0).getHistory().size());
|
||||
}
|
||||
|
||||
@@ -128,7 +129,7 @@ class StatusPageServiceImplTimeTest {
|
||||
when(historyDao.findStatusPageHistoriesByComponentIdAndTimestampBetween(anyLong(), anyLong(), anyLong()))
|
||||
.thenReturn(List.of(history));
|
||||
|
||||
List<ComponentStatus> result = service.queryComponentsStatus();
|
||||
List<ComponentStatus> result = service.queryComponentsStatus(30);
|
||||
assertEquals(30, result.get(0).getHistory().size());
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ resourceRole:
|
||||
- /api/sse/**===post===[admin,user]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin,user]
|
||||
- /api/logs/sse/**===get===[admin,user,guest]
|
||||
- /api/logs/ingest/**===post===[admin,user]
|
||||
- /api/otlp/**===post===[admin,user]
|
||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||
@@ -81,10 +82,10 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alert/sse/**===*
|
||||
- /api/logs/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
- /api/observability/capability===get
|
||||
- /api/push/**===*
|
||||
- /api/status/page/public/**===*
|
||||
- /api/manager/sse/**===*
|
||||
|
||||
@@ -70,6 +70,7 @@ resourceRole:
|
||||
- /api/mcp/**===post===[admin]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin]
|
||||
- /api/logs/sse/**===get===[admin,user,guest]
|
||||
- /api/logs/ingest/**===post===[admin,user]
|
||||
- /api/otlp/**===post===[admin,user]
|
||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||
@@ -84,10 +85,10 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alert/sse/**===*
|
||||
- /api/logs/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
- /api/observability/capability===get
|
||||
- /api/push/**===*
|
||||
- /api/status/page/public/**===*
|
||||
- /api/manager/sse/**===*
|
||||
|
||||
+3
-1
@@ -38,6 +38,8 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -79,7 +81,7 @@ public class GreptimeSqlQueryExecutor extends SqlQueryExecutor {
|
||||
headers.add(HttpHeaders.AUTHORIZATION, NetworkConstants.BASIC + SignConstants.BLANK + encodedAuth);
|
||||
}
|
||||
|
||||
String requestBody = "sql=" + queryString;
|
||||
String requestBody = "sql=" + URLEncoder.encode(queryString, StandardCharsets.UTF_8);
|
||||
HttpEntity<String> httpEntity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
String url = greptimeProperties.httpEndpoint() + QUERY_PATH;
|
||||
|
||||
+7
@@ -33,6 +33,13 @@ public interface HistoryDataReader {
|
||||
*/
|
||||
boolean isServerAvailable();
|
||||
|
||||
/**
|
||||
* @return whether this storage supports observability log query
|
||||
*/
|
||||
default boolean supportsLogQuery() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* query history range metrics data from tsdb
|
||||
*
|
||||
|
||||
+5
@@ -662,6 +662,11 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsLogQuery() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> queryLogOverviewAggregate(LogQueryFilter filter) {
|
||||
try {
|
||||
|
||||
-19
@@ -15,25 +15,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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.warehouse.store.history.tsdb.vm;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
+36
@@ -20,12 +20,16 @@
|
||||
package org.apache.hertzbeat.warehouse.db;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -34,6 +38,7 @@ import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeSqlQue
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpEntity;
|
||||
@@ -90,6 +95,37 @@ class GreptimeSqlQueryExecutorTest {
|
||||
assertEquals(85.5, result.get(0).get("value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testRequestBodyIsFormEncoded() {
|
||||
when(restTemplate.exchange(
|
||||
any(String.class),
|
||||
eq(HttpMethod.POST),
|
||||
any(HttpEntity.class),
|
||||
eq(GreptimeSqlQueryContent.class)
|
||||
)).thenReturn(new ResponseEntity<>(new GreptimeSqlQueryContent(), HttpStatus.OK));
|
||||
|
||||
// SQL carrying form-urlencoded metacharacters that would pollute the request body
|
||||
// if concatenated raw: '&' would start a second sql= parameter and override the query.
|
||||
String sql = "SELECT * FROM t WHERE name = 'x&sql=DROP TABLE t'";
|
||||
greptimeSqlQueryExecutor.execute(sql);
|
||||
|
||||
ArgumentCaptor<HttpEntity<String>> captor = ArgumentCaptor.forClass(HttpEntity.class);
|
||||
verify(restTemplate).exchange(
|
||||
any(String.class),
|
||||
eq(HttpMethod.POST),
|
||||
captor.capture(),
|
||||
eq(GreptimeSqlQueryContent.class));
|
||||
|
||||
String body = captor.getValue().getBody();
|
||||
assertNotNull(body);
|
||||
// The body must be a single sql= parameter; the '&' inside the SQL must be encoded.
|
||||
String encodedValue = body.substring("sql=".length());
|
||||
assertFalse(encodedValue.contains("&"), "raw '&' in body causes HTTP parameter pollution");
|
||||
// Decoding the value must yield exactly the original SQL, lossless round-trip.
|
||||
assertEquals(sql, URLDecoder.decode(encodedValue, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExecuteError() {
|
||||
// Mock error response
|
||||
|
||||
@@ -24,7 +24,7 @@ module.exports = {
|
||||
},
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['zh-cn', 'en'],
|
||||
locales: ['zh-cn', 'en', 'ko'],
|
||||
},
|
||||
themeConfig: {
|
||||
matomo: {
|
||||
|
||||
@@ -0,0 +1,504 @@
|
||||
{
|
||||
"theme.NotFound.title": {
|
||||
"message": "페이지를 찾을 수 없습니다",
|
||||
"description": "The title of the 404 page"
|
||||
},
|
||||
"theme.NotFound.p1": {
|
||||
"message": "찾으시는 페이지를 찾을 수 없습니다.",
|
||||
"description": "The first paragraph of the 404 page"
|
||||
},
|
||||
"theme.NotFound.p2": {
|
||||
"message": "이 URL로 연결한 사이트의 관리자에게 링크가 깨졌다고 알려주세요.",
|
||||
"description": "The 2nd paragraph of the 404 page"
|
||||
},
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": {
|
||||
"message": "닫기",
|
||||
"description": "The ARIA label for close button of announcement bar"
|
||||
},
|
||||
"theme.blog.paginator.navAriaLabel": {
|
||||
"message": "블로그 목록 페이지 내비게이션",
|
||||
"description": "The ARIA label for the blog pagination"
|
||||
},
|
||||
"theme.blog.paginator.newerEntries": {
|
||||
"message": "최신 글",
|
||||
"description": "The label used to navigate to the newer blog posts page (previous page)"
|
||||
},
|
||||
"theme.blog.paginator.olderEntries": {
|
||||
"message": "이전 글",
|
||||
"description": "The label used to navigate to the older blog posts page (next page)"
|
||||
},
|
||||
"theme.blog.post.readingTime.plurals": {
|
||||
"message": "{readingTime}분 소요",
|
||||
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.tags.tagsListLabel": {
|
||||
"message": "태그:",
|
||||
"description": "The label alongside a tag list"
|
||||
},
|
||||
"theme.blog.post.readMore": {
|
||||
"message": "더 읽기",
|
||||
"description": "The label used in blog post item excerpts to link to full blog posts"
|
||||
},
|
||||
"theme.blog.post.paginator.navAriaLabel": {
|
||||
"message": "블로그 게시글 페이지 내비게이션",
|
||||
"description": "The ARIA label for the blog posts pagination"
|
||||
},
|
||||
"theme.blog.post.paginator.newerPost": {
|
||||
"message": "다음 글",
|
||||
"description": "The blog post button label to navigate to the newer/previous post"
|
||||
},
|
||||
"theme.blog.post.paginator.olderPost": {
|
||||
"message": "이전 글",
|
||||
"description": "The blog post button label to navigate to the older/next post"
|
||||
},
|
||||
"theme.blog.post.plurals": {
|
||||
"message": "게시글 {count}개",
|
||||
"description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.blog.tagTitle": {
|
||||
"message": "\"{tagName}\" 태그가 지정된 {nPosts}",
|
||||
"description": "The title of the page for a blog tag"
|
||||
},
|
||||
"theme.tags.tagsPageLink": {
|
||||
"message": "모든 태그 보기",
|
||||
"description": "The label of the link targeting the tag list page"
|
||||
},
|
||||
"theme.CodeBlock.copyButtonAriaLabel": {
|
||||
"message": "코드를 클립보드에 복사",
|
||||
"description": "The ARIA label for copy code blocks button"
|
||||
},
|
||||
"theme.CodeBlock.copied": {
|
||||
"message": "복사됨",
|
||||
"description": "The copied button label on code blocks"
|
||||
},
|
||||
"theme.CodeBlock.copy": {
|
||||
"message": "복사",
|
||||
"description": "The copy button label on code blocks"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonTitle": {
|
||||
"message": "사이드바 펼치기",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": {
|
||||
"message": "사이드바 펼치기",
|
||||
"description": "The ARIA label and title attribute for expand button of doc sidebar"
|
||||
},
|
||||
"theme.docs.paginator.navAriaLabel": {
|
||||
"message": "문서 페이지 내비게이션",
|
||||
"description": "The ARIA label for the docs pagination"
|
||||
},
|
||||
"theme.docs.paginator.previous": {
|
||||
"message": "이전",
|
||||
"description": "The label used to navigate to the previous doc"
|
||||
},
|
||||
"theme.docs.paginator.next": {
|
||||
"message": "다음",
|
||||
"description": "The label used to navigate to the next doc"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonTitle": {
|
||||
"message": "사이드바 접기",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": {
|
||||
"message": "사이드바 접기",
|
||||
"description": "The title attribute for collapse button of doc sidebar"
|
||||
},
|
||||
"theme.docs.versions.unreleasedVersionLabel": {
|
||||
"message": "이 문서는 아직 출시되지 않은 {siteTitle} {versionLabel} 버전에 대한 것입니다.",
|
||||
"description": "The label used to tell the user that he's browsing an unreleased doc version"
|
||||
},
|
||||
"theme.docs.versions.unmaintainedVersionLabel": {
|
||||
"message": "이 문서는 더 이상 관리되지 않는 {siteTitle} {versionLabel} 버전에 대한 것입니다.",
|
||||
"description": "The label used to tell the user that he's browsing an unmaintained doc version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": {
|
||||
"message": "최신 문서는 {latestVersionLink}({versionLabel})을 참고하세요.",
|
||||
"description": "The label used to tell the user to check the latest version"
|
||||
},
|
||||
"theme.docs.versions.latestVersionLinkLabel": {
|
||||
"message": "최신 버전",
|
||||
"description": "The label used for the latest version suggestion link label"
|
||||
},
|
||||
"theme.common.editThisPage": {
|
||||
"message": "이 페이지 수정하기",
|
||||
"description": "The link label to edit the current page"
|
||||
},
|
||||
"theme.common.headingLinkTitle": {
|
||||
"message": "제목으로 바로가기 링크",
|
||||
"description": "Title for link to heading"
|
||||
},
|
||||
"theme.lastUpdated.atDate": {
|
||||
"message": " {date}에",
|
||||
"description": "The words used to describe on which date a page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.byUser": {
|
||||
"message": " {user}에 의해",
|
||||
"description": "The words used to describe by who the page has been last updated"
|
||||
},
|
||||
"theme.lastUpdated.lastUpdatedAtBy": {
|
||||
"message": "마지막 업데이트{atDate}{byUser}",
|
||||
"description": "The sentence used to display when a page has been last updated, and by who"
|
||||
},
|
||||
"theme.common.skipToMainContent": {
|
||||
"message": "본문으로 건너뛰기",
|
||||
"description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation"
|
||||
},
|
||||
"theme.SearchPage.documentsFound.plurals": {
|
||||
"message": "문서 {count}개 검색됨",
|
||||
"description": "Pluralized label for \"{count} documents found\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.SearchPage.existingResultsTitle": {
|
||||
"message": "\"{query}\" 검색 결과",
|
||||
"description": "The search page title for non-empty query"
|
||||
},
|
||||
"theme.SearchPage.emptyResultsTitle": {
|
||||
"message": "문서 검색",
|
||||
"description": "The search page title for empty query"
|
||||
},
|
||||
"theme.SearchPage.inputPlaceholder": {
|
||||
"message": "검색어를 입력하세요",
|
||||
"description": "The placeholder for search page input"
|
||||
},
|
||||
"theme.SearchPage.inputLabel": {
|
||||
"message": "검색",
|
||||
"description": "The ARIA label for search page input"
|
||||
},
|
||||
"theme.SearchPage.algoliaLabel": {
|
||||
"message": "Algolia 검색",
|
||||
"description": "The ARIA label for Algolia mention"
|
||||
},
|
||||
"theme.SearchPage.noResultsText": {
|
||||
"message": "검색 결과가 없습니다",
|
||||
"description": "The paragraph for empty search result"
|
||||
},
|
||||
"theme.SearchPage.fetchingNewResults": {
|
||||
"message": "새 검색 결과를 가져오는 중...",
|
||||
"description": "The paragraph for fetching new search results"
|
||||
},
|
||||
"theme.SearchBar.label": {
|
||||
"message": "검색",
|
||||
"description": "The ARIA label and placeholder for search button"
|
||||
},
|
||||
"theme.PwaReloadPopup.info": {
|
||||
"message": "새 버전이 있습니다",
|
||||
"description": "The text for PWA reload popup"
|
||||
},
|
||||
"theme.PwaReloadPopup.refreshButtonText": {
|
||||
"message": "새로고침",
|
||||
"description": "The text for PWA reload button"
|
||||
},
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": {
|
||||
"message": "닫기",
|
||||
"description": "The ARIA label for close button of PWA reload popup"
|
||||
},
|
||||
"theme.Playground.result": {
|
||||
"message": "결과",
|
||||
"description": "The result label of the live codeblocks"
|
||||
},
|
||||
"theme.Playground.liveEditor": {
|
||||
"message": "라이브 에디터",
|
||||
"description": "The live editor label of the live codeblocks"
|
||||
},
|
||||
"convenient": {
|
||||
"message": "쉬운 사용성"
|
||||
},
|
||||
"custom-multi-support": {
|
||||
"message": "고성능 & 높은 확장성"
|
||||
},
|
||||
"opensource": {
|
||||
"message": "오픈소스 & 커뮤니티 주도"
|
||||
},
|
||||
"theme.ErrorPageContent.title": {
|
||||
"message": "이 페이지에서 오류가 발생했습니다.",
|
||||
"description": "The title of the fallback page when the page crashed"
|
||||
},
|
||||
"theme.ErrorPageContent.tryAgain": {
|
||||
"message": "다시 시도",
|
||||
"description": "The label of the button to try again when the page crashed"
|
||||
},
|
||||
"theme.BackToTopButton.buttonAriaLabel": {
|
||||
"message": "맨 위로 스크롤",
|
||||
"description": "The ARIA label for the back to top button"
|
||||
},
|
||||
"theme.blog.archive.title": {
|
||||
"message": "아카이브",
|
||||
"description": "The page & hero title of the blog archive page"
|
||||
},
|
||||
"theme.blog.archive.description": {
|
||||
"message": "아카이브",
|
||||
"description": "The page & hero description of the blog archive page"
|
||||
},
|
||||
"theme.blog.sidebar.navAriaLabel": {
|
||||
"message": "블로그 최근 게시글 내비게이션",
|
||||
"description": "The ARIA label for recent posts in the blog sidebar"
|
||||
},
|
||||
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": {
|
||||
"message": "'{label}' 사이드바 카테고리 접기/펼치기",
|
||||
"description": "The ARIA label to toggle the collapsible sidebar category"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle.nDocsTagged": {
|
||||
"message": "문서 {count}개",
|
||||
"description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
|
||||
},
|
||||
"theme.docs.tagDocListPageTitle": {
|
||||
"message": "\"{tagName}\" 태그가 지정된 {nDocsTagged}",
|
||||
"description": "The title of the page for a docs tag"
|
||||
},
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": {
|
||||
"message": "← 메인 메뉴로 돌아가기",
|
||||
"description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
|
||||
},
|
||||
"theme.navbar.mobileVersionsDropdown.label": {
|
||||
"message": "버전",
|
||||
"description": "The label for the navbar versions dropdown on mobile view"
|
||||
},
|
||||
"theme.TOCCollapsible.toggleButtonLabel": {
|
||||
"message": "이 페이지에서",
|
||||
"description": "The label used by the button on the collapsible TOC component"
|
||||
},
|
||||
"convenient-content": {
|
||||
"message": "{docker} {br} 올인원 모니터링, 알람, 알림 시스템. 웹 서비스, 데이터베이스, 캐시, OS, 미들웨어, 빅데이터, 클라우드 네이티브, 네트워크 모니터링을 지원합니다. {br} 에이전트리스 방식으로 웹 화면에서 클릭 몇 번만으로 시작할 수 있습니다. {br} 모든 민감 데이터에 대해 종단 간 암호화를 적용한 엔터프라이즈급 보안을 제공합니다."
|
||||
},
|
||||
"custom-multi-support-content": {
|
||||
"message": "HTTP, JMX, SSH, SNMP, JDBC 등 설정 가능한 프로토콜. 간단한 온라인 YAML 설정으로 커스텀 메트릭을 정의할 수 있습니다. {br} 수집기 클러스터의 수평 확장, 다중 네트워크 격리, 클라우드-엣지 협업을 지원하는 고성능 아키텍처입니다. {br} Discord, Slack, 이메일, Webhook 등을 통한 유연하고 신속한 알람 규칙 및 알림을 제공합니다."
|
||||
},
|
||||
"opensource-content": {
|
||||
"message": "Apache HertzBeat™는 포용적인 커뮤니티를 가진 오픈소스 프로젝트입니다. 코드, 문서, 버그 리포트 등 규모에 상관없이 모든 기여를 환영합니다. {br} 모든 기여자를 존중하며 함께 성장합니다. {br} 전 세계 수천 대의 서버에 배포되어 있습니다."
|
||||
},
|
||||
"slogan": {
|
||||
"message": "AI 기반 오픈소스 옵저버빌리티 시스템"
|
||||
},
|
||||
"Who uses HertzBeat?": {
|
||||
"message": "Apache HertzBeat는 누가 사용하나요?"
|
||||
},
|
||||
"theme.admonition.note": {
|
||||
"message": "참고",
|
||||
"description": "The default label used for the Note admonition (:::note)"
|
||||
},
|
||||
"theme.admonition.tip": {
|
||||
"message": "팁",
|
||||
"description": "The default label used for the Tip admonition (:::tip)"
|
||||
},
|
||||
"theme.admonition.danger": {
|
||||
"message": "위험",
|
||||
"description": "The default label used for the Danger admonition (:::danger)"
|
||||
},
|
||||
"theme.admonition.info": {
|
||||
"message": "정보",
|
||||
"description": "The default label used for the Info admonition (:::info)"
|
||||
},
|
||||
"theme.admonition.caution": {
|
||||
"message": "주의",
|
||||
"description": "The default label used for the Caution admonition (:::caution)"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel": {
|
||||
"message": "다크·라이트 모드 전환(현재 {mode})",
|
||||
"description": "The ARIA label for the navbar color mode toggle"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.dark": {
|
||||
"message": "다크 모드",
|
||||
"description": "The name for the dark color mode"
|
||||
},
|
||||
"theme.colorToggle.ariaLabel.mode.light": {
|
||||
"message": "라이트 모드",
|
||||
"description": "The name for the light color mode"
|
||||
},
|
||||
"theme.docs.breadcrumbs.home": {
|
||||
"message": "홈페이지",
|
||||
"description": "The ARIA label for the home page in the breadcrumbs"
|
||||
},
|
||||
"theme.docs.breadcrumbs.navAriaLabel": {
|
||||
"message": "이동 경로",
|
||||
"description": "The ARIA label for the breadcrumbs"
|
||||
},
|
||||
"theme.docs.DocCard.categoryDescription": {
|
||||
"message": "{count}개 항목",
|
||||
"description": "The default description for a category card in the generated index about how many items this category includes"
|
||||
},
|
||||
"theme.docs.versionBadge.label": {
|
||||
"message": "버전: {versionLabel}"
|
||||
},
|
||||
"theme.CodeBlock.wordWrapToggle": {
|
||||
"message": "줄바꿈 전환",
|
||||
"description": "The title attribute for toggle word wrapping button of code block lines"
|
||||
},
|
||||
"theme.navbar.mobileLanguageDropdown.label": {
|
||||
"message": "언어",
|
||||
"description": "The label for the mobile language switcher dropdown"
|
||||
},
|
||||
"theme.blog.post.readMoreLabel": {
|
||||
"message": "{title}에 대해 더 읽기",
|
||||
"description": "The ARIA label for the link to full blog posts from excerpts"
|
||||
},
|
||||
"theme.SearchBar.seeAll": {
|
||||
"message": "{count}개 결과 모두 보기"
|
||||
},
|
||||
"theme.SearchModal.searchBox.resetButtonTitle": {
|
||||
"message": "검색어 지우기",
|
||||
"description": "The label and ARIA label for search box reset button"
|
||||
},
|
||||
"theme.SearchModal.searchBox.cancelButtonText": {
|
||||
"message": "취소",
|
||||
"description": "The label and ARIA label for search box cancel button"
|
||||
},
|
||||
"theme.SearchModal.startScreen.recentSearchesTitle": {
|
||||
"message": "최근 검색",
|
||||
"description": "The title for recent searches"
|
||||
},
|
||||
"theme.SearchModal.startScreen.noRecentSearchesText": {
|
||||
"message": "최근 검색 기록이 없습니다",
|
||||
"description": "The text when no recent searches"
|
||||
},
|
||||
"theme.SearchModal.startScreen.saveRecentSearchButtonTitle": {
|
||||
"message": "이 검색 저장",
|
||||
"description": "The label for save recent search button"
|
||||
},
|
||||
"theme.SearchModal.startScreen.removeRecentSearchButtonTitle": {
|
||||
"message": "이 검색을 기록에서 삭제",
|
||||
"description": "The label for remove recent search button"
|
||||
},
|
||||
"theme.SearchModal.startScreen.favoriteSearchesTitle": {
|
||||
"message": "즐겨찾기",
|
||||
"description": "The title for favorite searches"
|
||||
},
|
||||
"theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle": {
|
||||
"message": "이 검색을 즐겨찾기에서 삭제",
|
||||
"description": "The label for remove favorite search button"
|
||||
},
|
||||
"theme.SearchModal.errorScreen.titleText": {
|
||||
"message": "결과를 가져올 수 없습니다",
|
||||
"description": "The title for error screen of search modal"
|
||||
},
|
||||
"theme.SearchModal.errorScreen.helpText": {
|
||||
"message": "네트워크 연결 상태를 확인해 보세요.",
|
||||
"description": "The help text for error screen of search modal"
|
||||
},
|
||||
"theme.SearchModal.footer.selectText": {
|
||||
"message": "선택",
|
||||
"description": "The explanatory text of the action for the enter key"
|
||||
},
|
||||
"theme.SearchModal.footer.selectKeyAriaLabel": {
|
||||
"message": "Enter 키",
|
||||
"description": "The ARIA label for the Enter key button that makes the selection"
|
||||
},
|
||||
"theme.SearchModal.footer.navigateText": {
|
||||
"message": "이동",
|
||||
"description": "The explanatory text of the action for the Arrow up and Arrow down key"
|
||||
},
|
||||
"theme.SearchModal.footer.navigateUpKeyAriaLabel": {
|
||||
"message": "위쪽 화살표",
|
||||
"description": "The ARIA label for the Arrow up key button that makes the navigation"
|
||||
},
|
||||
"theme.SearchModal.footer.navigateDownKeyAriaLabel": {
|
||||
"message": "아래쪽 화살표",
|
||||
"description": "The ARIA label for the Arrow down key button that makes the navigation"
|
||||
},
|
||||
"theme.SearchModal.footer.closeText": {
|
||||
"message": "닫기",
|
||||
"description": "The explanatory text of the action for Escape key"
|
||||
},
|
||||
"theme.SearchModal.footer.closeKeyAriaLabel": {
|
||||
"message": "Esc 키",
|
||||
"description": "The ARIA label for the Escape key button that close the modal"
|
||||
},
|
||||
"theme.SearchModal.footer.searchByText": {
|
||||
"message": "검색 제공",
|
||||
"description": "The text explain that the search is making by Algolia"
|
||||
},
|
||||
"theme.SearchModal.noResultsScreen.noResultsText": {
|
||||
"message": "검색 결과 없음:",
|
||||
"description": "The text explains that there are no results for the following search"
|
||||
},
|
||||
"theme.SearchModal.noResultsScreen.suggestedQueryText": {
|
||||
"message": "다음 검색어를 시도해 보세요:",
|
||||
"description": "The text for the suggested query when no results are found for the following search"
|
||||
},
|
||||
"theme.SearchModal.noResultsScreen.reportMissingResultsText": {
|
||||
"message": "이 검색어에 결과가 있어야 한다고 생각하시나요?",
|
||||
"description": "The text for the question where the user thinks there are missing results"
|
||||
},
|
||||
"theme.SearchModal.noResultsScreen.reportMissingResultsLinkText": {
|
||||
"message": "알려주세요.",
|
||||
"description": "The text for the link to report missing results"
|
||||
},
|
||||
"theme.SearchModal.placeholder": {
|
||||
"message": "문서 검색",
|
||||
"description": "The placeholder of the input of the DocSearch pop-up modal"
|
||||
},
|
||||
"theme.IdealImageMessage.loading": {
|
||||
"message": "로딩 중...",
|
||||
"description": "When the full-scale image is loading"
|
||||
},
|
||||
"theme.IdealImageMessage.load": {
|
||||
"message": "클릭하여 로드{sizeMessage}",
|
||||
"description": "To prompt users to load the full image. sizeMessage is a parenthesized size figure."
|
||||
},
|
||||
"theme.IdealImageMessage.offline": {
|
||||
"message": "오프라인 상태입니다. 이미지를 불러올 수 없습니다",
|
||||
"description": "When the user is viewing an offline document"
|
||||
},
|
||||
"theme.IdealImageMessage.404error": {
|
||||
"message": "404. 이미지를 찾을 수 없습니다",
|
||||
"description": "When the image is not found"
|
||||
},
|
||||
"theme.IdealImageMessage.error": {
|
||||
"message": "오류가 발생했습니다. 클릭하여 다시 로드",
|
||||
"description": "When the image fails to load for unknown error"
|
||||
},
|
||||
"support hertzbeat": {
|
||||
"message": "HertzBeat 후원하기"
|
||||
},
|
||||
"click": {
|
||||
"message": "클릭"
|
||||
},
|
||||
"go github": {
|
||||
"message": "Github로 이동"
|
||||
},
|
||||
"If you like HertzBeat, star us on GitHub": {
|
||||
"message": "HertzBeat가 마음에 드셨다면 GitHub에서 Star를 눌러주세요."
|
||||
},
|
||||
"We will grow better with your support.": {
|
||||
"message": "여러분의 응원과 함께 더 크게 성장하겠습니다. 가장 큰 힘이 됩니다."
|
||||
},
|
||||
"quickstart": {
|
||||
"message": "빠른 시작"
|
||||
},
|
||||
"Support HertzBeat": {
|
||||
"message": "HertzBeat 후원하기"
|
||||
},
|
||||
"Go to Github": {
|
||||
"message": "Github로 이동"
|
||||
},
|
||||
"theme.NavBar.navAriaLabel": {
|
||||
"message": "메인",
|
||||
"description": "The ARIA label for the main navigation"
|
||||
},
|
||||
"theme.docs.sidebar.navAriaLabel": {
|
||||
"message": "문서 사이드바",
|
||||
"description": "The ARIA label for the sidebar navigation"
|
||||
},
|
||||
"theme.docs.sidebar.closeSidebarButtonAriaLabel": {
|
||||
"message": "내비게이션 바 닫기",
|
||||
"description": "The ARIA label for close button of mobile sidebar"
|
||||
},
|
||||
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
|
||||
"message": "내비게이션 바 전환",
|
||||
"description": "The ARIA label for hamburger menu button of mobile navigation"
|
||||
},
|
||||
"theme.tags.tagsPageTitle": {
|
||||
"message": "태그",
|
||||
"description": "The title of the tag list page"
|
||||
},
|
||||
"team.name": {
|
||||
"message": "Apache HertzBeat™ 팀"
|
||||
},
|
||||
"team.desc": {
|
||||
"message": "HertzBeat 팀은 전 세계 다양한 분야의 기여자들로 구성되어 있습니다. 우리는 개방과 협업이라는 커뮤니티 철학을 존중하며, 더 많은 분들이 함께 참여하고👏 커뮤니티와 함께 성장하기를 환영합니다."
|
||||
},
|
||||
"team.tip": {
|
||||
"message": "(특정 순서 없음)"
|
||||
},
|
||||
"team.thanks": {
|
||||
"message": "HertzBeat에 기여해주신 멋진 분들께 감사드립니다!"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "블로그",
|
||||
"description": "The title for the blog used in SEO"
|
||||
},
|
||||
"description": {
|
||||
"message": "블로그",
|
||||
"description": "The description for the blog used in SEO"
|
||||
},
|
||||
"sidebar.title": {
|
||||
"message": "최근 게시글",
|
||||
"description": "The label for the left sidebar"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"version.label": {
|
||||
"message": "dev",
|
||||
"description": "The label for version current"
|
||||
},
|
||||
"sidebar.docs.category.quickstart": {
|
||||
"message": "빠른 시작",
|
||||
"description": "The label for category quickstart in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.custom": {
|
||||
"message": "커스텀 모니터링",
|
||||
"description": "The label for category custom in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.http": {
|
||||
"message": "HTTP 프로토콜",
|
||||
"description": "The label for category http in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.jdbc": {
|
||||
"message": "JDBC 프로토콜",
|
||||
"description": "The label for category jdbc in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.ssh": {
|
||||
"message": "SSH 프로토콜",
|
||||
"description": "The label for category ssh in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.telnet": {
|
||||
"message": "TELNET 프로토콜",
|
||||
"description": "The label for category telnet in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.guides": {
|
||||
"message": "사용 가이드",
|
||||
"description": "The label for category guides in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.service": {
|
||||
"message": "서비스 모니터링",
|
||||
"description": "The label for category service in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.database": {
|
||||
"message": "데이터베이스 모니터링",
|
||||
"description": "The label for category database in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.os": {
|
||||
"message": "OS 모니터링",
|
||||
"description": "The label for category os in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.server": {
|
||||
"message": "서버 모니터링",
|
||||
"description": "The label for category server in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.alert": {
|
||||
"message": "알람",
|
||||
"description": "The label for category alert in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.notice": {
|
||||
"message": "알림",
|
||||
"description": "The label for category notice in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.service-discovery": {
|
||||
"message": "서비스 디스커버리",
|
||||
"description": "The label for category service-discovery in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.mid": {
|
||||
"message": "미들웨어 모니터링",
|
||||
"description": "The label for category mid in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.Others": {
|
||||
"message": "기타",
|
||||
"description": "The label for category Others in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.cloud-native": {
|
||||
"message": "클라우드 네이티브 모니터링",
|
||||
"description": "The label for category cloud-native in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.jmx": {
|
||||
"message": "JMX 프로토콜",
|
||||
"description": "The label for category jmx in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.snmp": {
|
||||
"message": "SNMP 프로토콜",
|
||||
"description": "The label for category snmp in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.link.Install via Helm": {
|
||||
"message": "Helm으로 설치",
|
||||
"description": "The label for link Install via Helm in sidebar docs, linking to https://artifacthub.io/packages/helm/hertzbeat/hertzbeat"
|
||||
},
|
||||
"sidebar.docs.category.push": {
|
||||
"message": "Push",
|
||||
"description": "The label for category push in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.program": {
|
||||
"message": "애플리케이션 모니터링",
|
||||
"description": "The label for category program in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.cache": {
|
||||
"message": "캐시 모니터링",
|
||||
"description": "The label for category cache in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.bigdata": {
|
||||
"message": "빅데이터 모니터링",
|
||||
"description": "The label for category bigdata in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.webserver": {
|
||||
"message": "웹서버 모니터링",
|
||||
"description": "The label for category webserver in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.contribution": {
|
||||
"message": "기여 가이드",
|
||||
"description": "The label for category contribution in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.submit": {
|
||||
"message": "제출 가이드",
|
||||
"description": "The label for category submit in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.release": {
|
||||
"message": "릴리스 가이드",
|
||||
"description": "The label for category release in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.Community": {
|
||||
"message": "커뮤니티",
|
||||
"description": "The label for category Community in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.network": {
|
||||
"message": "네트워크 모니터링",
|
||||
"description": "The label for category network in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.NGQL": {
|
||||
"message": "NGQL",
|
||||
"description": "The label for category NGQL in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.llm": {
|
||||
"message": "AI LLM 모니터링",
|
||||
"description": "The label for category llm in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.aiConfig": {
|
||||
"message": "AI LLM 설정",
|
||||
"description": "The label for category aiConfig in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.install": {
|
||||
"message": "설치",
|
||||
"description": "The label for category install in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.upgrade": {
|
||||
"message": "업그레이드",
|
||||
"description": "The label for category upgrade in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.setting": {
|
||||
"message": "설정",
|
||||
"description": "The label for category setting in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.change-db": {
|
||||
"message": "DB 변경",
|
||||
"description": "The label for category change-db in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.use-case": {
|
||||
"message": "사용 사례",
|
||||
"description": "The label for category use-case in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.extern-deploy": {
|
||||
"message": "외부 설치",
|
||||
"description": "The label for category extern-deploy in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.log": {
|
||||
"message": "로그",
|
||||
"description": "The label for category log in sidebar docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"version.label": {
|
||||
"message": "1.8.0",
|
||||
"description": "The label for version current"
|
||||
},
|
||||
"sidebar.docs.category.quickstart": {
|
||||
"message": "빠른 시작",
|
||||
"description": "The label for category quickstart in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.custom": {
|
||||
"message": "커스텀 모니터링",
|
||||
"description": "The label for category custom in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.http": {
|
||||
"message": "HTTP 프로토콜",
|
||||
"description": "The label for category http in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.jdbc": {
|
||||
"message": "JDBC 프로토콜",
|
||||
"description": "The label for category jdbc in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.ssh": {
|
||||
"message": "SSH 프로토콜",
|
||||
"description": "The label for category ssh in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.telnet": {
|
||||
"message": "TELNET 프로토콜",
|
||||
"description": "The label for category telnet in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.guides": {
|
||||
"message": "사용 가이드",
|
||||
"description": "The label for category guides in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.service": {
|
||||
"message": "서비스 모니터링",
|
||||
"description": "The label for category service in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.database": {
|
||||
"message": "데이터베이스 모니터링",
|
||||
"description": "The label for category database in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.os": {
|
||||
"message": "OS 모니터링",
|
||||
"description": "The label for category os in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.server": {
|
||||
"message": "서버 모니터링",
|
||||
"description": "The label for category server in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.alert": {
|
||||
"message": "알람",
|
||||
"description": "The label for category alert in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.notice": {
|
||||
"message": "알림",
|
||||
"description": "The label for category notice in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.service-discovery": {
|
||||
"message": "서비스 디스커버리",
|
||||
"description": "The label for category service-discovery in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.mid": {
|
||||
"message": "미들웨어 모니터링",
|
||||
"description": "The label for category mid in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.Others": {
|
||||
"message": "기타",
|
||||
"description": "The label for category Others in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.cloud-native": {
|
||||
"message": "클라우드 네이티브 모니터링",
|
||||
"description": "The label for category cloud-native in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.jmx": {
|
||||
"message": "JMX 프로토콜",
|
||||
"description": "The label for category jmx in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.snmp": {
|
||||
"message": "SNMP 프로토콜",
|
||||
"description": "The label for category snmp in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.link.Install via Helm": {
|
||||
"message": "Helm으로 설치",
|
||||
"description": "The label for link Install via Helm in sidebar docs, linking to https://artifacthub.io/packages/helm/hertzbeat/hertzbeat"
|
||||
},
|
||||
"sidebar.docs.category.push": {
|
||||
"message": "Push",
|
||||
"description": "The label for category push in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.program": {
|
||||
"message": "애플리케이션 모니터링",
|
||||
"description": "The label for category program in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.cache": {
|
||||
"message": "캐시 모니터링",
|
||||
"description": "The label for category cache in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.bigdata": {
|
||||
"message": "빅데이터 모니터링",
|
||||
"description": "The label for category bigdata in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.webserver": {
|
||||
"message": "웹서버 모니터링",
|
||||
"description": "The label for category webserver in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.contribution": {
|
||||
"message": "기여 가이드",
|
||||
"description": "The label for category contribution in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.submit": {
|
||||
"message": "제출 가이드",
|
||||
"description": "The label for category submit in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.release": {
|
||||
"message": "릴리스 가이드",
|
||||
"description": "The label for category release in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.Community": {
|
||||
"message": "커뮤니티",
|
||||
"description": "The label for category Community in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.network": {
|
||||
"message": "네트워크 모니터링",
|
||||
"description": "The label for category network in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.NGQL": {
|
||||
"message": "NGQL",
|
||||
"description": "The label for category NGQL in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.llm": {
|
||||
"message": "AI LLM 모니터링",
|
||||
"description": "The label for category llm in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.aiConfig": {
|
||||
"message": "AI LLM 설정",
|
||||
"description": "The label for category aiConfig in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.install": {
|
||||
"message": "설치",
|
||||
"description": "The label for category install in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.upgrade": {
|
||||
"message": "업그레이드",
|
||||
"description": "The label for category upgrade in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.setting": {
|
||||
"message": "설정",
|
||||
"description": "The label for category setting in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.change-db": {
|
||||
"message": "DB 변경",
|
||||
"description": "The label for category change-db in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.use-case": {
|
||||
"message": "사용 사례",
|
||||
"description": "The label for category use-case in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.extern-deploy": {
|
||||
"message": "외부 설치",
|
||||
"description": "The label for category extern-deploy in sidebar docs"
|
||||
},
|
||||
"sidebar.docs.category.log": {
|
||||
"message": "로그",
|
||||
"description": "The label for category log in sidebar docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"link.title.intro": {
|
||||
"message": "소개",
|
||||
"description": "The title of the footer links column with title=intro in the footer"
|
||||
},
|
||||
"link.title.contact": {
|
||||
"message": "커뮤니티",
|
||||
"description": "The title of the footer links column with title=contact in the footer"
|
||||
},
|
||||
"link.item.label.use": {
|
||||
"message": "소개",
|
||||
"description": "The label of footer link with label=use linking to /docs/"
|
||||
},
|
||||
"link.item.label.quickstart": {
|
||||
"message": "빠른 시작",
|
||||
"description": "The label of footer link with label=quickstart linking to /docs/start/quickstart"
|
||||
},
|
||||
"link.item.label.custom": {
|
||||
"message": "커스텀 모니터링",
|
||||
"description": "The label of footer link with label=custom linking to /docs/advanced/extend-point"
|
||||
},
|
||||
"link.item.label.help": {
|
||||
"message": "도움말 문서",
|
||||
"description": "The label of footer link with label=help linking to /docs/help/guide"
|
||||
},
|
||||
"link.item.label.discuss": {
|
||||
"message": "문의",
|
||||
"description": "The label of footer link with label=discuss linking to /docs/others/contact"
|
||||
},
|
||||
"link.item.label.github": {
|
||||
"message": "Github",
|
||||
"description": "The label of footer link with label=github linking to https://github.com/apache/hertzbeat"
|
||||
},
|
||||
"link.title.resource": {
|
||||
"message": "자료",
|
||||
"description": "The title of the footer links column with title=resource in the footer"
|
||||
},
|
||||
"link.item.label.relate": {
|
||||
"message": "기타",
|
||||
"description": "The label of footer link with label=relate linking to /docs/others/resource"
|
||||
},
|
||||
"link.item.label.discussion": {
|
||||
"message": "토론",
|
||||
"description": "The label of footer link with label=discussion linking to https://github.com/apache/hertzbeat/discussions/"
|
||||
},
|
||||
"link.item.label.contact": {
|
||||
"message": "문의",
|
||||
"description": "The label of footer link with label=contact linking to /docs/community/contact"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"title": {
|
||||
"message": "HertzBeat",
|
||||
"description": "The title in the navbar"
|
||||
},
|
||||
"item.label.main": {
|
||||
"message": "홈",
|
||||
"description": "Navbar item with label main"
|
||||
},
|
||||
"item.label.document": {
|
||||
"message": "문서",
|
||||
"description": "Navbar item with label document"
|
||||
},
|
||||
"item.label.blog": {
|
||||
"message": "블로그",
|
||||
"description": "Navbar item with label blog"
|
||||
},
|
||||
"item.label.discuss": {
|
||||
"message": "커뮤니티 문의",
|
||||
"description": "Navbar item with label discuss"
|
||||
},
|
||||
"item.label.kanban": {
|
||||
"message": "칸반",
|
||||
"description": "Navbar item with label kanban"
|
||||
},
|
||||
"item.label.about": {
|
||||
"message": "소개",
|
||||
"description": "Navbar item with label about"
|
||||
},
|
||||
"item.label.login": {
|
||||
"message": "지금 로그인",
|
||||
"description": "Navbar item with label login"
|
||||
},
|
||||
"item.label.design": {
|
||||
"message": "설계",
|
||||
"description": "Navbar item with label design"
|
||||
},
|
||||
"item.label.resource": {
|
||||
"message": "자료",
|
||||
"description": "Navbar item with label resource"
|
||||
},
|
||||
"item.label.contributing": {
|
||||
"message": "기여자 가이드",
|
||||
"description": "Navbar item with label contributing"
|
||||
},
|
||||
"item.label.developer": {
|
||||
"message": "기여자",
|
||||
"description": "Navbar item with label developer"
|
||||
},
|
||||
"item.label.enterprise": {
|
||||
"message": "엔터프라이즈",
|
||||
"description": "Navbar item with label enterprise"
|
||||
},
|
||||
"item.label.Docs": {
|
||||
"message": "문서",
|
||||
"description": "Navbar item with label Docs"
|
||||
},
|
||||
"item.label.Others": {
|
||||
"message": "기타",
|
||||
"description": "Navbar item with label Others"
|
||||
},
|
||||
"item.label.contributors": {
|
||||
"message": "기여자",
|
||||
"description": "Navbar item with label contributors"
|
||||
},
|
||||
"item.label.ASF": {
|
||||
"message": "ASF",
|
||||
"description": "Navbar item with label ASF"
|
||||
},
|
||||
"item.label.Community": {
|
||||
"message": "커뮤니티",
|
||||
"description": "Navbar item with label Community"
|
||||
},
|
||||
"item.label.code_of_conduct": {
|
||||
"message": "행동 강령",
|
||||
"description": "Navbar item with label code_of_conduct"
|
||||
},
|
||||
"item.label.join_the_mailing_lists": {
|
||||
"message": "메일링 리스트 가입",
|
||||
"description": "Navbar item with label join_the_mailing_lists"
|
||||
},
|
||||
"item.label.become_committer": {
|
||||
"message": "커미터 되기",
|
||||
"description": "Navbar item with label become_committer"
|
||||
},
|
||||
"item.label.become_pmc_member": {
|
||||
"message": "PMC 멤버 되기",
|
||||
"description": "Navbar item with label become_pmc_member"
|
||||
},
|
||||
"item.label.new_committer_process": {
|
||||
"message": "신규 커미터 절차",
|
||||
"description": "Navbar item with label new_committer_process"
|
||||
},
|
||||
"item.label.new_pmc_member_process": {
|
||||
"message": "신규 PMC 멤버 절차",
|
||||
"description": "Navbar item with label new_pmc_member_process"
|
||||
},
|
||||
"item.label.documentation_notice": {
|
||||
"message": "문서 안내",
|
||||
"description": "Navbar item with label documentation_notice"
|
||||
},
|
||||
"item.label.submit_code": {
|
||||
"message": "코드 제출",
|
||||
"description": "Navbar item with label submit_code"
|
||||
},
|
||||
"item.label.code_style_and_quality_guide": {
|
||||
"message": "코드 스타일 및 품질 가이드",
|
||||
"description": "Navbar item with label code_style_and_quality_guide"
|
||||
},
|
||||
"item.label.how_to_release": {
|
||||
"message": "릴리스 방법",
|
||||
"description": "Navbar item with label how_to_release"
|
||||
},
|
||||
"item.label.how_to_verify_release": {
|
||||
"message": "릴리스 검증 방법",
|
||||
"description": "Navbar item with label how_to_verify_release"
|
||||
},
|
||||
"item.label.Foundation": {
|
||||
"message": "재단",
|
||||
"description": "Navbar item with label Foundation"
|
||||
},
|
||||
"item.label.License": {
|
||||
"message": "라이선스",
|
||||
"description": "Navbar item with label License"
|
||||
},
|
||||
"item.label.Events": {
|
||||
"message": "이벤트",
|
||||
"description": "Navbar item with label Events"
|
||||
},
|
||||
"item.label.Security": {
|
||||
"message": "보안",
|
||||
"description": "Navbar item with label Security"
|
||||
},
|
||||
"item.label.Sponsorship": {
|
||||
"message": "후원",
|
||||
"description": "Navbar item with label Sponsorship"
|
||||
},
|
||||
"item.label.Privacy": {
|
||||
"message": "개인정보처리방침",
|
||||
"description": "Navbar item with label Privacy"
|
||||
},
|
||||
"item.label.team": {
|
||||
"message": "팀",
|
||||
"description": "Navbar item with label team"
|
||||
},
|
||||
"item.label.Thanks": {
|
||||
"message": "감사의 말",
|
||||
"description": "Navbar item with label Thanks"
|
||||
},
|
||||
"item.label.download": {
|
||||
"message": "다운로드",
|
||||
"description": "Navbar item with label download"
|
||||
},
|
||||
"item.label.contribution": {
|
||||
"message": "기여 가이드",
|
||||
"description": "Navbar item with label contribution"
|
||||
},
|
||||
"item.label.development": {
|
||||
"message": "개발",
|
||||
"description": "Navbar item with label development"
|
||||
}
|
||||
}
|
||||
@@ -2983,5 +2983,231 @@
|
||||
"code"
|
||||
],
|
||||
"githubId": "183058331"
|
||||
},
|
||||
{
|
||||
"login": "orangeCatDeveloper",
|
||||
"name": "NekoPunch",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95899648?v=4",
|
||||
"profile": "https://github.com/orangeCatDeveloper",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
],
|
||||
"githubId": "95899648"
|
||||
},
|
||||
{
|
||||
"login": "wy471x",
|
||||
"name": "wy471x",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/52033069?v=4",
|
||||
"profile": "https://wy471x.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "52033069"
|
||||
},
|
||||
{
|
||||
"login": "hengyuss",
|
||||
"name": "hengyuss",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/81064732?v=4",
|
||||
"profile": "https://github.com/hengyuss",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "81064732"
|
||||
},
|
||||
{
|
||||
"login": "moduvoice",
|
||||
"name": "moduvoice",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/291867022?v=4",
|
||||
"profile": "https://moduvoice.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
],
|
||||
"githubId": "291867022"
|
||||
},
|
||||
{
|
||||
"login": "hutiefang76",
|
||||
"name": "hutiefang76",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/137664623?v=4",
|
||||
"profile": "https://github.com/hutiefang76",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "137664623"
|
||||
},
|
||||
{
|
||||
"login": "ZhouYinLong-lab",
|
||||
"name": "柳含知 Liu Hanzhi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/250777154?v=4",
|
||||
"profile": "https://zylatent.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
],
|
||||
"githubId": "250777154"
|
||||
},
|
||||
{
|
||||
"login": "wilmerdooley",
|
||||
"name": "wilmerdooley",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/259930736?v=4",
|
||||
"profile": "https://github.com/wilmerdooley",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "259930736"
|
||||
},
|
||||
{
|
||||
"login": "Zmjjeff7",
|
||||
"name": "Zhenhong Guo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/175370943?v=4",
|
||||
"profile": "https://github.com/Zmjjeff7",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "175370943"
|
||||
},
|
||||
{
|
||||
"login": "abhyudayareddy",
|
||||
"name": "abhyudayareddy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/54602866?v=4",
|
||||
"profile": "https://github.com/abhyudayareddy",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "54602866"
|
||||
},
|
||||
{
|
||||
"login": "neon-hippo",
|
||||
"name": "neon-hippo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/165560498?v=4",
|
||||
"profile": "https://github.com/neon-hippo",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "165560498"
|
||||
},
|
||||
{
|
||||
"login": "P-Peaceful",
|
||||
"name": "P_Peaceful",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/52856161?v=4",
|
||||
"profile": "https://github.com/P-Peaceful",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
],
|
||||
"githubId": "52856161"
|
||||
},
|
||||
{
|
||||
"login": "zhusaidong",
|
||||
"name": "zhusaidong",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3039961?v=4",
|
||||
"profile": "https://github.com/zhusaidong",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "3039961"
|
||||
},
|
||||
{
|
||||
"login": "zhehenlu",
|
||||
"name": "zhlu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31504542?v=4",
|
||||
"profile": "https://github.com/zhehenlu",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
],
|
||||
"githubId": "31504542"
|
||||
},
|
||||
{
|
||||
"login": "brettgervasoni",
|
||||
"name": "brettgervasoni",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/34056000?v=4",
|
||||
"profile": "https://github.com/brettgervasoni",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "34056000"
|
||||
},
|
||||
{
|
||||
"login": "Darshan-paul",
|
||||
"name": "Darshan-paul",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/211450705?v=4",
|
||||
"profile": "https://github.com/Darshan-paul",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "211450705"
|
||||
},
|
||||
{
|
||||
"login": "04cb",
|
||||
"name": "layla",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/111667698?v=4",
|
||||
"profile": "https://github.com/04cb",
|
||||
"contributions": [
|
||||
"doc"
|
||||
],
|
||||
"githubId": "111667698"
|
||||
},
|
||||
{
|
||||
"login": "miantalha45",
|
||||
"name": "Talha Amjad",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/155809113?v=4",
|
||||
"profile": "https://github.com/miantalha45",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "155809113"
|
||||
},
|
||||
{
|
||||
"login": "turanalmammadov",
|
||||
"name": "Turan Almammadov",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16321061?v=4",
|
||||
"profile": "https://turanalmammadov.com/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
],
|
||||
"githubId": "16321061"
|
||||
},
|
||||
{
|
||||
"login": "zhaoyangplus",
|
||||
"name": "zhaoyangplus",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/245090302?v=4",
|
||||
"profile": "https://github.com/zhaoyangplus",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "245090302"
|
||||
},
|
||||
{
|
||||
"login": "yexuanyang",
|
||||
"name": "Yang Yexuan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/73885401?v=4",
|
||||
"profile": "https://github.com/yexuanyang",
|
||||
"contributions": [
|
||||
"code"
|
||||
],
|
||||
"githubId": "73885401"
|
||||
},
|
||||
{
|
||||
"login": "markguo123",
|
||||
"name": "markguo123",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/155072651?v=4",
|
||||
"profile": "https://github.com/markguo123",
|
||||
"contributions": [
|
||||
"doc"
|
||||
],
|
||||
"githubId": "155072651"
|
||||
},
|
||||
{
|
||||
"login": "leo-934",
|
||||
"name": "leo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/55838224?v=4",
|
||||
"profile": "https://github.com/leo-934",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"blog"
|
||||
],
|
||||
"githubId": "55838224"
|
||||
}
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -42,7 +42,7 @@ LIMIT <1-100>
|
||||
|
||||
服务支持通过 `greptime.username` 和 `greptime.password` 发送 HTTP Basic Authentication。用户名和密码必须同时配置;建议配合 HTTPS 或可信内网使用。
|
||||
|
||||
项目当前 Docker Compose 使用 GreptimeDB `v0.14.3`,该版本只提供身份认证,不能限制用户为只读权限。因此当前真正生效的安全边界是“删除原始 SQL参数并固定生成单条 `SELECT`”。使用 GreptimeDB 1.0 及以上版本时,应为该 MCP 配置独立的 `ro`/`readonly` 账号。
|
||||
项目当前 Docker Compose 使用 GreptimeDB `v1.1.3`。应为该 MCP 配置独立的 `ro`/`readonly` 账号;同时,服务仍通过删除原始 SQL 参数并固定生成单条 `SELECT`,限制可执行查询范围。
|
||||
|
||||
## Claude Desktop 集成(stdio)
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@
|
||||
<influxdb.version>2.23</influxdb.version>
|
||||
<spring-cloud-starter-openfeign.version>3.0.5</spring-cloud-starter-openfeign.version>
|
||||
<greptimedb.version>0.11.0</greptimedb.version>
|
||||
<grpc.version>1.76.3</grpc.version>
|
||||
<arrow.version>18.1.0</arrow.version>
|
||||
<snappy-java.version>1.1.10.7</snappy-java.version>
|
||||
<sshd-sftp.version>2.13.1</sshd-sftp.version>
|
||||
@@ -579,6 +580,14 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- grpc: managed -->
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-bom</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ resourceRole:
|
||||
- /api/mcp/**===post===[admin]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin]
|
||||
- /api/logs/sse/**===get===[admin,user,guest]
|
||||
- /api/logs/ingest/**===post===[admin,user]
|
||||
|
||||
# config the resource restful api that need bypass auth protection
|
||||
@@ -77,10 +78,10 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alert/sse/**===*
|
||||
- /api/logs/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
- /api/observability/capability===get
|
||||
- /api/push/**===*
|
||||
- /api/status/page/public/**===*
|
||||
- /api/manager/sse/**===*
|
||||
|
||||
@@ -70,6 +70,7 @@ resourceRole:
|
||||
- /api/mcp/**===post===[admin]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin]
|
||||
- /api/logs/sse/**===get===[admin,user,guest]
|
||||
- /api/logs/ingest/**===post===[admin,user]
|
||||
|
||||
# config the resource restful api that need bypass auth protection
|
||||
@@ -77,10 +78,10 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alert/sse/**===*
|
||||
- /api/logs/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
- /api/observability/capability===get
|
||||
- /api/push/**===*
|
||||
- /api/status/page/public/**===*
|
||||
- /api/manager/sse/**===*
|
||||
|
||||
@@ -70,6 +70,7 @@ resourceRole:
|
||||
- /api/mcp/**===post===[admin]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin]
|
||||
- /api/logs/sse/**===get===[admin,user,guest]
|
||||
- /api/logs/ingest/**===post===[admin,user]
|
||||
|
||||
# config the resource restful api that need bypass auth protection
|
||||
@@ -77,10 +78,10 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alert/sse/**===*
|
||||
- /api/logs/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
- /api/observability/capability===get
|
||||
- /api/push/**===*
|
||||
- /api/status/page/public/**===*
|
||||
- /api/manager/sse/**===*
|
||||
|
||||
@@ -70,6 +70,7 @@ resourceRole:
|
||||
- /api/mcp/**===post===[admin]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin]
|
||||
- /api/logs/sse/**===get===[admin,user,guest]
|
||||
- /api/logs/ingest/**===post===[admin,user]
|
||||
- /api/otlp/**===post===[admin,user]
|
||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||
@@ -81,10 +82,10 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alert/sse/**===*
|
||||
- /api/logs/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
- /api/observability/capability===get
|
||||
- /api/push/**===*
|
||||
- /api/status/page/public/**===*
|
||||
- /api/manager/sse/**===*
|
||||
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
- hertzbeat
|
||||
|
||||
greptime:
|
||||
image: greptime/greptimedb:v0.14.3
|
||||
image: greptime/greptimedb:v1.1.3
|
||||
container_name: compose-greptimedb
|
||||
hostname: greptime
|
||||
restart: always
|
||||
|
||||
@@ -70,6 +70,7 @@ resourceRole:
|
||||
- /api/mcp/**===post===[admin]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin]
|
||||
- /api/logs/sse/**===get===[admin,user,guest]
|
||||
- /api/logs/ingest/**===post===[admin,user]
|
||||
|
||||
# config the resource restful api that need bypass auth protection
|
||||
@@ -77,10 +78,10 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alert/sse/**===*
|
||||
- /api/logs/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
- /api/observability/capability===get
|
||||
- /api/push/**===*
|
||||
- /api/status/page/public/**===*
|
||||
- /api/manager/sse/**===*
|
||||
|
||||
+2
-1
@@ -70,6 +70,7 @@ resourceRole:
|
||||
- /api/mcp/**===post===[admin]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin]
|
||||
- /api/logs/sse/**===get===[admin,user,guest]
|
||||
- /api/logs/ingest/**===post===[admin,user]
|
||||
- /api/otlp/**===post===[admin,user]
|
||||
- /api/ingestion/otlp/**===get===[admin,user,guest]
|
||||
@@ -81,10 +82,10 @@ resourceRole:
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
excludedResource:
|
||||
- /api/alert/sse/**===*
|
||||
- /api/logs/sse/**===*
|
||||
- /api/account/auth/**===*
|
||||
- /api/i18n/**===get
|
||||
- /api/apps/hierarchy===get
|
||||
- /api/observability/capability===get
|
||||
- /api/push/**===*
|
||||
- /api/status/page/public/**===*
|
||||
- /api/manager/sse/**===*
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"customSyntax": "postcss-less",
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-rational-order",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"lint": "npm run lint:fix && npm run lint:style",
|
||||
"lint:ts": "ng lint",
|
||||
"lint:fix": "ng lint --fix",
|
||||
"lint:style": "stylelint \"src/**/*.less\" --syntax less --fix",
|
||||
"lint:style": "stylelint \"src/**/*.less\" --fix",
|
||||
"package": "ng build --configuration production"
|
||||
},
|
||||
"private": true,
|
||||
@@ -124,6 +124,7 @@
|
||||
"lint-staged": "13.3.0",
|
||||
"ng-alain": "17.3.1",
|
||||
"node-fetch": "2.7.0",
|
||||
"postcss-less": "6.0.0",
|
||||
"prettier": "2.8.8",
|
||||
"protractor": "7.0.0",
|
||||
"source-map-explorer": "2.5.3",
|
||||
|
||||
Generated
+73
-49
@@ -255,6 +255,9 @@ importers:
|
||||
node-fetch:
|
||||
specifier: 2.7.0
|
||||
version: 2.7.0(encoding@0.1.13)
|
||||
postcss-less:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0(postcss@8.5.6)
|
||||
prettier:
|
||||
specifier: 2.8.8
|
||||
version: 2.8.8
|
||||
@@ -1674,24 +1677,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@nx/nx-linux-arm64-musl@19.8.14':
|
||||
resolution: {integrity: sha512-ltty/PDWqkYgu/6Ye65d7v5nh3D6e0n3SacoKRs2Vtfz5oHYRUkSKizKIhEVfRNuHn3d9j8ve1fdcCN4SDPUBQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@nx/nx-linux-x64-gnu@19.8.14':
|
||||
resolution: {integrity: sha512-JzE3BuO9RCBVdgai18CCze6KUzG0AozE0TtYFxRokfSC05NU3nUhd/o62UsOl7s6Bqt/9nwrW7JC8pNDiCi9OQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@nx/nx-linux-x64-musl@19.8.14':
|
||||
resolution: {integrity: sha512-2rPvDOQLb7Wd6YiU88FMBiLtYco0dVXF99IJBRGAWv+WTI7MNr47OyK2ze+JOsbYY1d8aOGUvckUvCCZvZKEfg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@nx/nx-win32-arm64-msvc@19.8.14':
|
||||
resolution: {integrity: sha512-JxW+YPS+EjhUsLw9C6wtk9pQTG3psyFwxhab8y/dgk2s4AOTLyIm0XxgcCJVvB6i4uv+s1g0QXRwp6+q3IR6hg==}
|
||||
@@ -1743,66 +1750,79 @@ packages:
|
||||
resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.55.1':
|
||||
resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.55.1':
|
||||
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.55.1':
|
||||
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.55.1':
|
||||
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.55.1':
|
||||
resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.55.1':
|
||||
resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.55.1':
|
||||
resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.55.1':
|
||||
resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.55.1':
|
||||
resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.55.1':
|
||||
resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.55.1':
|
||||
resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.55.1':
|
||||
resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.55.1':
|
||||
resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
|
||||
@@ -2251,6 +2271,7 @@ packages:
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
'@vitejs/plugin-basic-ssl@1.1.0':
|
||||
resolution: {integrity: sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==}
|
||||
@@ -4125,11 +4146,12 @@ packages:
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
global-modules@2.0.0:
|
||||
resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
|
||||
@@ -7081,6 +7103,7 @@ packages:
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
temp@0.9.4:
|
||||
resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==}
|
||||
@@ -7426,11 +7449,12 @@ packages:
|
||||
|
||||
uuid@3.4.0:
|
||||
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
|
||||
deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
v8-compile-cache-lib@3.0.1:
|
||||
@@ -7813,7 +7837,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@angular-devkit/architect': 0.1703.8(chokidar@3.6.0)
|
||||
'@angular-devkit/build-webpack': 0.1703.8(chokidar@3.6.0)(webpack-dev-server@4.15.1(webpack@5.90.3))(webpack@5.90.3)
|
||||
'@angular-devkit/build-webpack': 0.1703.8(chokidar@3.6.0)(webpack-dev-server@4.15.1(webpack@5.90.3(esbuild@0.20.1)))(webpack@5.90.3(esbuild@0.20.1))
|
||||
'@angular-devkit/core': 17.3.8(chokidar@3.6.0)
|
||||
'@angular/compiler-cli': 17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3)
|
||||
'@babel/core': 7.24.0
|
||||
@@ -7826,16 +7850,16 @@ snapshots:
|
||||
'@babel/preset-env': 7.24.0(@babel/core@7.24.0)
|
||||
'@babel/runtime': 7.24.0
|
||||
'@discoveryjs/json-ext': 0.5.7
|
||||
'@ngtools/webpack': 17.3.8(@angular/compiler-cli@17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3))(typescript@5.3.3)(webpack@5.90.3)
|
||||
'@ngtools/webpack': 17.3.8(@angular/compiler-cli@17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3))(typescript@5.3.3)(webpack@5.90.3(esbuild@0.20.1))
|
||||
'@vitejs/plugin-basic-ssl': 1.1.0(vite@5.1.7(@types/node@20.12.11)(less@4.2.0)(sass@1.71.1)(sugarss@2.0.0)(terser@5.29.1))
|
||||
ansi-colors: 4.1.3
|
||||
autoprefixer: 10.4.18(postcss@8.4.35)
|
||||
babel-loader: 9.1.3(@babel/core@7.24.0)(webpack@5.90.3)
|
||||
babel-loader: 9.1.3(@babel/core@7.24.0)(webpack@5.90.3(esbuild@0.20.1))
|
||||
babel-plugin-istanbul: 6.1.1
|
||||
browserslist: 4.28.1
|
||||
copy-webpack-plugin: 11.0.0(webpack@5.90.3)
|
||||
copy-webpack-plugin: 11.0.0(webpack@5.90.3(esbuild@0.20.1))
|
||||
critters: 0.0.22
|
||||
css-loader: 6.10.0(webpack@5.90.3)
|
||||
css-loader: 6.10.0(webpack@5.90.3(esbuild@0.20.1))
|
||||
esbuild-wasm: 0.20.1
|
||||
fast-glob: 3.3.2
|
||||
http-proxy-middleware: 2.0.6(@types/express@4.17.25)
|
||||
@@ -7844,11 +7868,11 @@ snapshots:
|
||||
jsonc-parser: 3.2.1
|
||||
karma-source-map-support: 1.4.0
|
||||
less: 4.2.0
|
||||
less-loader: 11.1.0(less@4.2.0)(webpack@5.90.3)
|
||||
license-webpack-plugin: 4.0.2(webpack@5.90.3)
|
||||
less-loader: 11.1.0(less@4.2.0)(webpack@5.90.3(esbuild@0.20.1))
|
||||
license-webpack-plugin: 4.0.2(webpack@5.90.3(esbuild@0.20.1))
|
||||
loader-utils: 3.2.1
|
||||
magic-string: 0.30.8
|
||||
mini-css-extract-plugin: 2.8.1(webpack@5.90.3)
|
||||
mini-css-extract-plugin: 2.8.1(webpack@5.90.3(esbuild@0.20.1))
|
||||
mrmime: 2.0.0
|
||||
open: 8.4.2
|
||||
ora: 5.4.1
|
||||
@@ -7856,13 +7880,13 @@ snapshots:
|
||||
picomatch: 4.0.1
|
||||
piscina: 4.4.0
|
||||
postcss: 8.4.35
|
||||
postcss-loader: 8.1.1(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.3)
|
||||
postcss-loader: 8.1.1(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.3(esbuild@0.20.1))
|
||||
resolve-url-loader: 5.0.0
|
||||
rxjs: 7.8.1
|
||||
sass: 1.71.1
|
||||
sass-loader: 14.1.1(sass@1.71.1)(webpack@5.90.3)
|
||||
sass-loader: 14.1.1(sass@1.71.1)(webpack@5.90.3(esbuild@0.20.1))
|
||||
semver: 7.6.0
|
||||
source-map-loader: 5.0.0(webpack@5.90.3)
|
||||
source-map-loader: 5.0.0(webpack@5.90.3(esbuild@0.20.1))
|
||||
source-map-support: 0.5.21
|
||||
terser: 5.29.1
|
||||
tree-kill: 1.2.2
|
||||
@@ -7872,10 +7896,10 @@ snapshots:
|
||||
vite: 5.1.7(@types/node@20.12.11)(less@4.2.0)(sass@1.71.1)(sugarss@2.0.0)(terser@5.29.1)
|
||||
watchpack: 2.4.0
|
||||
webpack: 5.90.3(esbuild@0.20.1)
|
||||
webpack-dev-middleware: 6.1.2(webpack@5.90.3)
|
||||
webpack-dev-server: 4.15.1(webpack@5.90.3)
|
||||
webpack-dev-middleware: 6.1.2(webpack@5.90.3(esbuild@0.20.1))
|
||||
webpack-dev-server: 4.15.1(webpack@5.90.3(esbuild@0.20.1))
|
||||
webpack-merge: 5.10.0
|
||||
webpack-subresource-integrity: 5.1.0(webpack@5.90.3)
|
||||
webpack-subresource-integrity: 5.1.0(webpack@5.90.3(esbuild@0.20.1))
|
||||
optionalDependencies:
|
||||
esbuild: 0.20.1
|
||||
karma: 6.4.2
|
||||
@@ -7899,12 +7923,12 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- webpack-cli
|
||||
|
||||
'@angular-devkit/build-webpack@0.1703.8(chokidar@3.6.0)(webpack-dev-server@4.15.1(webpack@5.90.3))(webpack@5.90.3)':
|
||||
'@angular-devkit/build-webpack@0.1703.8(chokidar@3.6.0)(webpack-dev-server@4.15.1(webpack@5.90.3(esbuild@0.20.1)))(webpack@5.90.3(esbuild@0.20.1))':
|
||||
dependencies:
|
||||
'@angular-devkit/architect': 0.1703.8(chokidar@3.6.0)
|
||||
rxjs: 7.8.1
|
||||
webpack: 5.90.3(esbuild@0.20.1)
|
||||
webpack-dev-server: 4.15.1(webpack@5.90.3)
|
||||
webpack-dev-server: 4.15.1(webpack@5.90.3(esbuild@0.20.1))
|
||||
transitivePeerDependencies:
|
||||
- chokidar
|
||||
|
||||
@@ -9393,7 +9417,7 @@ snapshots:
|
||||
'@emnapi/runtime': 1.8.1
|
||||
'@tybys/wasm-util': 0.9.0
|
||||
|
||||
'@ngtools/webpack@17.3.8(@angular/compiler-cli@17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3))(typescript@5.3.3)(webpack@5.90.3)':
|
||||
'@ngtools/webpack@17.3.8(@angular/compiler-cli@17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3))(typescript@5.3.3)(webpack@5.90.3(esbuild@0.20.1))':
|
||||
dependencies:
|
||||
'@angular/compiler-cli': 17.3.10(@angular/compiler@17.3.10(@angular/core@17.3.10(rxjs@7.8.1)(zone.js@0.14.5)))(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
@@ -10503,7 +10527,7 @@ snapshots:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
babel-loader@9.1.3(@babel/core@7.24.0)(webpack@5.90.3):
|
||||
babel-loader@9.1.3(@babel/core@7.24.0)(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
'@babel/core': 7.24.0
|
||||
find-cache-dir: 4.0.0
|
||||
@@ -10986,7 +11010,7 @@ snapshots:
|
||||
|
||||
copy-descriptor@0.1.1: {}
|
||||
|
||||
copy-webpack-plugin@11.0.0(webpack@5.90.3):
|
||||
copy-webpack-plugin@11.0.0(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
fast-glob: 3.3.2
|
||||
glob-parent: 6.0.2
|
||||
@@ -11075,7 +11099,7 @@ snapshots:
|
||||
|
||||
css-functions-list@3.2.3: {}
|
||||
|
||||
css-loader@6.10.0(webpack@5.90.3):
|
||||
css-loader@6.10.0(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
icss-utils: 5.1.0(postcss@8.4.35)
|
||||
postcss: 8.4.35
|
||||
@@ -13277,7 +13301,7 @@ snapshots:
|
||||
layout-base@2.0.1:
|
||||
optional: true
|
||||
|
||||
less-loader@11.1.0(less@4.2.0)(webpack@5.90.3):
|
||||
less-loader@11.1.0(less@4.2.0)(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
klona: 2.0.6
|
||||
less: 4.2.0
|
||||
@@ -13326,7 +13350,7 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
license-webpack-plugin@4.0.2(webpack@5.90.3):
|
||||
license-webpack-plugin@4.0.2(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
webpack-sources: 3.3.3
|
||||
optionalDependencies:
|
||||
@@ -13639,7 +13663,7 @@ snapshots:
|
||||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
mini-css-extract-plugin@2.8.1(webpack@5.90.3):
|
||||
mini-css-extract-plugin@2.8.1(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.0
|
||||
@@ -14443,17 +14467,17 @@ snapshots:
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39):
|
||||
postcss-html@0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39):
|
||||
dependencies:
|
||||
htmlparser2: 3.10.1
|
||||
postcss: 7.0.39
|
||||
postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39))(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39)
|
||||
postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39)
|
||||
|
||||
postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39):
|
||||
postcss-jsx@0.36.4(postcss-syntax@0.36.2)(postcss@7.0.39):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
postcss: 7.0.39
|
||||
postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39))(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39)
|
||||
postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -14465,7 +14489,7 @@ snapshots:
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
|
||||
postcss-loader@8.1.1(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.3):
|
||||
postcss-loader@8.1.1(postcss@8.4.35)(typescript@5.3.3)(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
cosmiconfig: 9.0.0(typescript@5.3.3)
|
||||
jiti: 1.21.7
|
||||
@@ -14476,10 +14500,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39):
|
||||
postcss-markdown@0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39):
|
||||
dependencies:
|
||||
postcss: 7.0.39
|
||||
postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39))(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39)
|
||||
postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39)
|
||||
remark: 10.0.1
|
||||
unist-util-find-all-after: 1.0.5
|
||||
|
||||
@@ -14557,14 +14581,14 @@ snapshots:
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
|
||||
postcss-syntax@0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39))(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39):
|
||||
postcss-syntax@0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39):
|
||||
dependencies:
|
||||
postcss: 7.0.39
|
||||
optionalDependencies:
|
||||
postcss-html: 0.36.0(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39)
|
||||
postcss-jsx: 0.36.4(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39)
|
||||
postcss-html: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39)
|
||||
postcss-jsx: 0.36.4(postcss-syntax@0.36.2)(postcss@7.0.39)
|
||||
postcss-less: 3.1.4
|
||||
postcss-markdown: 0.36.0(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39)
|
||||
postcss-markdown: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39)
|
||||
postcss-scss: 2.1.1
|
||||
|
||||
postcss-value-parser@3.3.1: {}
|
||||
@@ -15033,7 +15057,7 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sass-loader@14.1.1(sass@1.71.1)(webpack@5.90.3):
|
||||
sass-loader@14.1.1(sass@1.71.1)(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
neo-async: 2.6.2
|
||||
optionalDependencies:
|
||||
@@ -15375,7 +15399,7 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-loader@5.0.0(webpack@5.90.3):
|
||||
source-map-loader@5.0.0(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
source-map-js: 1.2.1
|
||||
@@ -15705,10 +15729,10 @@ snapshots:
|
||||
normalize-selector: 0.2.0
|
||||
pify: 4.0.1
|
||||
postcss: 7.0.39
|
||||
postcss-html: 0.36.0(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39)
|
||||
postcss-jsx: 0.36.4(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39)
|
||||
postcss-html: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39)
|
||||
postcss-jsx: 0.36.4(postcss-syntax@0.36.2)(postcss@7.0.39)
|
||||
postcss-less: 3.1.4
|
||||
postcss-markdown: 0.36.0(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39)
|
||||
postcss-markdown: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39)
|
||||
postcss-media-query-parser: 0.2.3
|
||||
postcss-reporter: 6.0.1
|
||||
postcss-resolve-nested-selector: 0.1.6
|
||||
@@ -15716,7 +15740,7 @@ snapshots:
|
||||
postcss-sass: 0.3.5
|
||||
postcss-scss: 2.1.1
|
||||
postcss-selector-parser: 3.1.2
|
||||
postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.6))(postcss@7.0.39))(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39)
|
||||
postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39)
|
||||
postcss-value-parser: 3.3.1
|
||||
resolve-from: 4.0.0
|
||||
signal-exit: 3.0.7
|
||||
@@ -15838,7 +15862,7 @@ snapshots:
|
||||
mkdirp: 0.5.6
|
||||
rimraf: 2.6.3
|
||||
|
||||
terser-webpack-plugin@5.3.16(esbuild@0.20.1)(webpack@5.90.3):
|
||||
terser-webpack-plugin@5.3.16(esbuild@0.20.1)(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
@@ -16281,7 +16305,7 @@ snapshots:
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webpack-dev-middleware@5.3.4(webpack@5.90.3):
|
||||
webpack-dev-middleware@5.3.4(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
colorette: 2.0.20
|
||||
memfs: 3.5.3
|
||||
@@ -16290,7 +16314,7 @@ snapshots:
|
||||
schema-utils: 4.3.3
|
||||
webpack: 5.90.3(esbuild@0.20.1)
|
||||
|
||||
webpack-dev-middleware@6.1.2(webpack@5.90.3):
|
||||
webpack-dev-middleware@6.1.2(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
colorette: 2.0.20
|
||||
memfs: 3.5.3
|
||||
@@ -16300,7 +16324,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
webpack: 5.90.3(esbuild@0.20.1)
|
||||
|
||||
webpack-dev-server@4.15.1(webpack@5.90.3):
|
||||
webpack-dev-server@4.15.1(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
'@types/bonjour': 3.5.13
|
||||
'@types/connect-history-api-fallback': 1.5.4
|
||||
@@ -16330,7 +16354,7 @@ snapshots:
|
||||
serve-index: 1.9.1
|
||||
sockjs: 0.3.24
|
||||
spdy: 4.0.2
|
||||
webpack-dev-middleware: 5.3.4(webpack@5.90.3)
|
||||
webpack-dev-middleware: 5.3.4(webpack@5.90.3(esbuild@0.20.1))
|
||||
ws: 8.19.0
|
||||
optionalDependencies:
|
||||
webpack: 5.90.3(esbuild@0.20.1)
|
||||
@@ -16348,7 +16372,7 @@ snapshots:
|
||||
|
||||
webpack-sources@3.3.3: {}
|
||||
|
||||
webpack-subresource-integrity@5.1.0(webpack@5.90.3):
|
||||
webpack-subresource-integrity@5.1.0(webpack@5.90.3(esbuild@0.20.1)):
|
||||
dependencies:
|
||||
typed-assert: 1.0.9
|
||||
webpack: 5.90.3(esbuild@0.20.1)
|
||||
@@ -16376,7 +16400,7 @@ snapshots:
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 3.3.0
|
||||
tapable: 2.3.0
|
||||
terser-webpack-plugin: 5.3.16(esbuild@0.20.1)(webpack@5.90.3)
|
||||
terser-webpack-plugin: 5.3.16(esbuild@0.20.1)(webpack@5.90.3(esbuild@0.20.1))
|
||||
watchpack: 2.4.0
|
||||
webpack-sources: 3.3.3
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -3,6 +3,7 @@ import { registerLocaleData } from '@angular/common';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import ngEn from '@angular/common/locales/en';
|
||||
import ngJa from '@angular/common/locales/ja';
|
||||
import ngKo from '@angular/common/locales/ko';
|
||||
import ngPt from '@angular/common/locales/pt';
|
||||
import ngZh from '@angular/common/locales/zh';
|
||||
import ngZhTw from '@angular/common/locales/zh-Hant';
|
||||
@@ -15,10 +16,11 @@ import {
|
||||
SettingsService,
|
||||
zh_CN as delonZhCn,
|
||||
zh_TW as delonZhTw,
|
||||
ja_JP as delonJaJP
|
||||
ja_JP as delonJaJP,
|
||||
ko_KR as delonKoKR
|
||||
} from '@delon/theme';
|
||||
import { AlainConfigService } from '@delon/util/config';
|
||||
import { enUS as dfEn, zhCN as dfZhCn, zhTW as dfZhTw, ja as dfJa, ptBR as dfPtBR } from 'date-fns/locale';
|
||||
import { enUS as dfEn, zhCN as dfZhCn, zhTW as dfZhTw, ja as dfJa, ko as dfKo, ptBR as dfPtBR } from 'date-fns/locale';
|
||||
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||
import {
|
||||
en_US as zorroEnUS,
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
zh_CN as zorroZhCN,
|
||||
zh_TW as zorroZhTW,
|
||||
ja_JP as zorroJaJP,
|
||||
ko_KR as zorroKoKR,
|
||||
pt_BR as zorroPtBR
|
||||
} from 'ng-zorro-antd/i18n';
|
||||
import { Observable, zip } from 'rxjs';
|
||||
@@ -76,6 +79,14 @@ const LANGS: { [key: string]: LangConfigData } = {
|
||||
delon: delonJaJP,
|
||||
abbr: '🇯🇵'
|
||||
},
|
||||
'ko-KR': {
|
||||
text: '한국어',
|
||||
ng: ngKo,
|
||||
zorro: zorroKoKR,
|
||||
date: dfKo,
|
||||
delon: delonKoKR,
|
||||
abbr: '🇰🇷'
|
||||
},
|
||||
'pt-BR': {
|
||||
text: 'Português (Brasil)',
|
||||
ng: ngPt,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
import { ALAIN_I18N_TOKEN, Menu, MenuService, SettingsService, TitleService } from '@delon/theme';
|
||||
import type { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||
import { NzIconService } from 'ng-zorro-antd/icon';
|
||||
import { Observable, zip } from 'rxjs';
|
||||
import { Observable, of, zip } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
|
||||
import { ICONS } from '../../../style-icons';
|
||||
@@ -41,14 +41,15 @@ export class StartupService {
|
||||
return zip(
|
||||
this.i18n.loadLangData(defaultLang),
|
||||
this.httpClient.get('./assets/app-data.json', { headers: headers }),
|
||||
this.httpClient.get(`/apps/hierarchy?lang=${defaultLang}`)
|
||||
this.httpClient.get(`/apps/hierarchy?lang=${defaultLang}`),
|
||||
this.httpClient.get('/observability/capability').pipe(catchError(() => of(null)))
|
||||
).pipe(
|
||||
catchError((res: NzSafeAny) => {
|
||||
console.warn(`StartupService.load: Network request failed`, res);
|
||||
setTimeout(() => this.router.navigateByUrl(`/exception/500`));
|
||||
return [];
|
||||
}),
|
||||
map(([langData, appData, menuData]: [Record<string, string>, NzSafeAny, NzSafeAny]) => {
|
||||
map(([langData, appData, menuData, capabilityData]: [Record<string, string>, NzSafeAny, NzSafeAny, NzSafeAny]) => {
|
||||
// setting language data
|
||||
this.i18n.use(defaultLang, langData);
|
||||
// Application information: including site name, description, year
|
||||
@@ -85,6 +86,7 @@ export class StartupService {
|
||||
item.hide = true;
|
||||
}
|
||||
});
|
||||
this.storageService.putData('observabilityCapability', capabilityData?.data ?? null);
|
||||
this.storageService.putData('hierarchy', menuData.data);
|
||||
this.menuService.resume();
|
||||
this.titleService.suffix = appData.app.name;
|
||||
|
||||
@@ -74,6 +74,12 @@ describe('SettingDrawerI18nDirective', () => {
|
||||
'setting.drawer.theme.color': '主題色',
|
||||
'setting.drawer.settings': '設置',
|
||||
'setting.drawer.info.message': '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改參數配置文件 src/styles/theme.less'
|
||||
},
|
||||
'ko-KR': {
|
||||
'setting.drawer.theme.color': '테마 색상',
|
||||
'setting.drawer.settings': '설정',
|
||||
'setting.drawer.info.message':
|
||||
'이 설정 패널은 개발 환경에서 미리보기 용도로만 제공되며, 운영 환경에서는 표시되지 않습니다. src/styles/theme.less 파일을 복사하여 직접 수정해 주세요'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,7 +127,7 @@ describe('SettingDrawerI18nDirective', () => {
|
||||
it('should load mappings from i18n files', fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const languages = ['zh-CN', 'en-US', 'ja-JP', 'pt-BR', 'zh-TW'];
|
||||
const languages = ['zh-CN', 'en-US', 'ja-JP', 'pt-BR', 'zh-TW', 'ko-KR'];
|
||||
const requests = languages.map(lang => httpMock.expectOne(`./assets/i18n/${lang}.json`));
|
||||
|
||||
languages.forEach((lang, index) => {
|
||||
@@ -137,7 +143,7 @@ describe('SettingDrawerI18nDirective', () => {
|
||||
it('should replace Chinese text with translations', fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const languages = ['zh-CN', 'en-US', 'ja-JP', 'pt-BR', 'zh-TW'];
|
||||
const languages = ['zh-CN', 'en-US', 'ja-JP', 'pt-BR', 'zh-TW', 'ko-KR'];
|
||||
const requests = languages.map(lang => httpMock.expectOne(`./assets/i18n/${lang}.json`));
|
||||
|
||||
languages.forEach((lang, index) => {
|
||||
|
||||
@@ -54,7 +54,7 @@ export class SettingDrawerI18nDirective implements OnInit, AfterViewInit, OnDest
|
||||
'setting.drawer.info.message'
|
||||
];
|
||||
|
||||
private readonly languages = ['zh-CN', 'en-US', 'ja-JP', 'pt-BR', 'zh-TW'];
|
||||
private readonly languages = ['zh-CN', 'en-US', 'ja-JP', 'pt-BR', 'zh-TW', 'ko-KR'];
|
||||
|
||||
constructor(
|
||||
private el: ElementRef<HTMLElement>,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<app-help-message-show
|
||||
[help_message_content]="'alert.help.setting' | i18n"
|
||||
[guild_link]="'alert.help.center.setting' | i18n"
|
||||
[guild_link]="'alert.help.setting.link' | i18n"
|
||||
[module_name]="'menu.alert.setting'"
|
||||
[icon_name]="'calculator'"
|
||||
></app-help-message-show>
|
||||
@@ -713,7 +713,7 @@
|
||||
[nzData]="previewData"
|
||||
[nzSize]="'small'"
|
||||
[nzLoading]="previewTableLoading"
|
||||
[nzScroll]="previewData.length > 3 ? { y: '180px' } : {}"
|
||||
[nzScroll]="previewData.length > 3 ? { x: 'max-content', y: '180px' } : { x: 'max-content' }"
|
||||
[nzShowPagination]="false"
|
||||
>
|
||||
<thead>
|
||||
@@ -788,7 +788,7 @@
|
||||
[nzData]="previewData"
|
||||
[nzSize]="'small'"
|
||||
[nzLoading]="previewTableLoading"
|
||||
[nzScroll]="previewData.length > 3 ? { y: '180px' } : {}"
|
||||
[nzScroll]="previewData.length > 3 ? { x: 'max-content', y: '180px' } : { x: 'max-content' }"
|
||||
[nzShowPagination]="false"
|
||||
>
|
||||
<thead>
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<section class="signal-page">
|
||||
<app-signal-storage-guide *ngIf="!storageSupported"></app-signal-storage-guide>
|
||||
<section class="signal-page" *ngIf="storageSupported">
|
||||
<header class="page-heading"
|
||||
><div
|
||||
><h1>{{ 'observability.logs.title' | i18n }}</h1
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { LogService } from '../../../service/log.service';
|
||||
import { MemoryStorageService } from '../../../service/memory-storage.service';
|
||||
import { LogManageComponent } from './log-manage.component';
|
||||
|
||||
describe('LogManageComponent', () => {
|
||||
@@ -27,7 +28,7 @@ describe('LogManageComponent', () => {
|
||||
const logs = jasmine.createSpyObj<LogService>('LogService', ['list', 'overviewStats', 'trendStats']);
|
||||
const route = { snapshot: { queryParamMap: { get: () => null }, data: {} } } as unknown as ActivatedRoute;
|
||||
const router = jasmine.createSpyObj<Router>('Router', ['navigate']);
|
||||
const component = new LogManageComponent(logs, route, router);
|
||||
const component = new LogManageComponent(logs, route, router, new MemoryStorageService());
|
||||
component.timeRange = [new Date(1_000), new Date(2_000)];
|
||||
|
||||
component.setMode('stream');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user